diff --git a/providers/aws/config/config.go b/providers/aws/config/config.go index 64cfa9c676..a3ea128b7a 100644 --- a/providers/aws/config/config.go +++ b/providers/aws/config/config.go @@ -6,6 +6,7 @@ package config import ( "go.mondoo.com/cnquery/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/providers/aws/connection" + "go.mondoo.com/cnquery/providers/aws/connection/awsec2ebsconn" "go.mondoo.com/cnquery/providers/aws/provider" ) @@ -13,14 +14,14 @@ var Config = plugin.Provider{ Name: "aws", ID: "go.mondoo.com/cnquery/providers/aws", Version: "9.0.0", - ConnectionTypes: []string{provider.DefaultConnectionType}, + ConnectionTypes: []string{provider.DefaultConnectionType, string(awsec2ebsconn.EBSConnectionType)}, Connectors: []plugin.Connector{ { Name: "aws", Use: "aws", Short: "aws account", MinArgs: 0, - MaxArgs: 0, + MaxArgs: 4, Discovery: []string{ connection.DiscoveryAccounts, connection.DiscoveryAll, @@ -77,6 +78,12 @@ var Config = plugin.Provider{ Default: "", Desc: "Endpoint URL override for authentication with the API", }, + { + Long: "no-setup", + Type: plugin.FlagType_String, + Default: "", + Desc: "Override option for EBS scanning that tells it to not create the snapshot or volume", + }, }, }, }, diff --git a/providers/aws/connection/awsec2ebsconn/destroy.go b/providers/aws/connection/awsec2ebsconn/destroy.go new file mode 100644 index 0000000000..0a84387426 --- /dev/null +++ b/providers/aws/connection/awsec2ebsconn/destroy.go @@ -0,0 +1,48 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package awsec2ebsconn + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/aws-sdk-go/aws" + "github.com/rs/zerolog/log" + awsec2ebstypes "go.mondoo.com/cnquery/providers/aws/connection/awsec2ebsconn/types" +) + +func (c *AwsEbsConnection) DetachVolumeFromInstance(ctx context.Context, volume *awsec2ebstypes.VolumeInfo) error { + log.Info().Msg("detach volume") + res, err := c.scannerRegionEc2svc.DetachVolume(ctx, &ec2.DetachVolumeInput{ + Device: aws.String(c.volumeMounter.VolumeAttachmentLoc), VolumeId: &volume.Id, + InstanceId: &c.scannerInstance.Id, + }) + if err != nil { + return err + } + if res.State != types.VolumeAttachmentStateDetached { // check if it's detached already + var volState types.VolumeState + for volState != types.VolumeStateAvailable { + time.Sleep(10 * time.Second) + resp, err := c.scannerRegionEc2svc.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{VolumeIds: []string{volume.Id}}) + if err != nil { + return err + } + if len(resp.Volumes) == 1 { + volState = resp.Volumes[0].State + + log.Info().Interface("state", volState).Msg("waiting for volume detachment completion") + } + } + } + return nil +} + +func (c *AwsEbsConnection) DeleteCreatedVolume(ctx context.Context, volume *awsec2ebstypes.VolumeInfo) error { + log.Info().Msg("delete created volume") + _, err := c.scannerRegionEc2svc.DeleteVolume(ctx, &ec2.DeleteVolumeInput{VolumeId: &volume.Id}) + return err +} diff --git a/providers/aws/connection/awsec2ebsconn/provider.go b/providers/aws/connection/awsec2ebsconn/provider.go new file mode 100644 index 0000000000..475aa2ce4f --- /dev/null +++ b/providers/aws/connection/awsec2ebsconn/provider.go @@ -0,0 +1,360 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package awsec2ebsconn + +import ( + "context" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/cockroachdb/errors" + "github.com/rs/zerolog/log" + "github.com/spf13/afero" + "go.mondoo.com/cnquery/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/providers-sdk/v1/util/convert" + awsec2ebstypes "go.mondoo.com/cnquery/providers/aws/connection/awsec2ebsconn/types" + "go.mondoo.com/cnquery/providers/os/connection" + "go.mondoo.com/cnquery/providers/os/connection/shared" + "go.mondoo.com/cnquery/providers/os/connection/snapshot" + "go.mondoo.com/cnquery/providers/os/detector" + "go.mondoo.com/cnquery/providers/os/id/awsec2" + "go.mondoo.com/cnquery/providers/os/id/ids" +) + +const ( + EBSConnectionType shared.ConnectionType = "ebs" +) + +type AwsEbsConnection struct { + id uint32 + asset *inventory.Asset + FsProvider *connection.FileSystemConnection + scannerRegionEc2svc *ec2.Client + targetRegionEc2svc *ec2.Client + config aws.Config + opts map[string]string + scannerInstance *awsec2ebstypes.InstanceId // the instance the transport is running on + scanVolumeInfo *awsec2ebstypes.VolumeInfo // the info of the volume we attached to the instance + target awsec2ebstypes.TargetInfo // info about the target + targetType string // the type of object we're targeting (instance, volume, snapshot) + volumeMounter *snapshot.VolumeMounter +} + +// New creates a new aws-ec2-ebs provider +// It expects to be running on an ec2 instance with ssm iam role and +// permissions for copy snapshot, create snapshot, create volume, attach volume, detach volume +func NewAwsEbsConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*AwsEbsConnection, error) { + log.Debug().Msg("new aws ebs connection") + // TODO: validate the expected permissions here + // TODO: allow custom aws config + // 1. validate; load + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + return nil, errors.Wrap(err, "could not load aws configuration") + } + i, err := RawInstanceInfo(cfg) + if err != nil { + return nil, errors.Wrap(err, "could not load instance info: aws-ec2-ebs provider only valid on ec2 instances") + } + + // ec2 client for the scanner region + cfg.Region = i.Region + scannerSvc := ec2.NewFromConfig(cfg) + + // ec2 client for the target region + cfgCopy := cfg.Copy() + cfgCopy.Region = conf.Options["region"] + targetSvc := ec2.NewFromConfig(cfgCopy) + + // 2. create provider instance + c := &AwsEbsConnection{ + config: cfg, + opts: conf.Options, + target: awsec2ebstypes.TargetInfo{ + PlatformId: conf.PlatformId, + Region: conf.Options["region"], + Id: conf.Options["id"], + }, + targetType: conf.Options["type"], + scannerInstance: &awsec2ebstypes.InstanceId{ + Id: i.InstanceID, + Region: i.Region, + Account: i.AccountID, + Zone: i.AvailabilityZone, + }, + targetRegionEc2svc: targetSvc, + scannerRegionEc2svc: scannerSvc, + asset: asset, + } + log.Debug().Interface("info", c.target).Str("type", c.targetType).Msg("target") + + ctx := context.Background() + + // 3. validate + instanceinfo, volumeid, snapshotid, err := c.Validate(ctx) + if err != nil { + return c, errors.Wrap(err, "unable to validate") + } + + // 4. setup the volume for scanning + // check if we got the no setup override option. this implies the target volume is already attached to the instance + // this is used in cases where we need to test a snapshot created from a public marketplace image. the volume gets attached to a brand + // new instance, and then that instance is started and we scan the attached fs + var volLocation string + if conf.Options[snapshot.NoSetup] == "true" || conf.Options[snapshot.IsSetup] == "true" { + log.Info().Msg("skipping setup step") + } else { + var ok bool + var err error + switch c.targetType { + case awsec2ebstypes.EBSTargetInstance: + ok, volLocation, err = c.SetupForTargetInstance(ctx, instanceinfo) + conf.PlatformId = awsec2.MondooInstanceID(i.AccountID, conf.Options["region"], convert.ToString(instanceinfo.InstanceId)) + case awsec2ebstypes.EBSTargetVolume: + ok, volLocation, err = c.SetupForTargetVolume(ctx, *volumeid) + conf.PlatformId = awsec2.MondooVolumeID(volumeid.Account, volumeid.Region, volumeid.Id) + case awsec2ebstypes.EBSTargetSnapshot: + ok, volLocation, err = c.SetupForTargetSnapshot(ctx, *snapshotid) + conf.PlatformId = awsec2.MondooSnapshotID(snapshotid.Account, snapshotid.Region, snapshotid.Id) + default: + return c, errors.New("invalid target type") + } + if err != nil { + log.Error().Err(err).Msg("unable to complete setup step") + c.Close() + return c, err + } + if !ok { + c.Close() + return c, errors.New("something went wrong; unable to complete setup for ebs volume scan") + } + // set is setup to true + asset.Connections[0].Options[snapshot.IsSetup] = "true" + } + asset.PlatformIds = []string{conf.PlatformId} + + // Mount Volume + shell := []string{"sh", "-c"} + volumeMounter := snapshot.NewVolumeMounter(shell) + volumeMounter.VolumeAttachmentLoc = volLocation + if conf.Options["mounted"] != "" { + log.Info().Msg("skipping mount step") + } else { + err = volumeMounter.Mount() + if err != nil { + log.Error().Err(err).Msg("unable to complete mount step") + c.Close() + return c, err + } + // set mounted + asset.Connections[0].Options["mounted"] = volumeMounter.ScanDir + } + if volumeMounter.ScanDir == "" && conf.Options["mounted"] != "" { + volumeMounter.ScanDir = conf.Options["mounted"] + } + if volumeMounter.ScanDir == "" { + c.Close() + return c, errors.New("no scan dir specified") + } + + log.Debug().Interface("info", c.target).Str("type", c.targetType).Msg("target") + + // Create and initialize fs provider + fsConn, err := connection.NewFileSystemConnection(id, &inventory.Config{ + Path: volumeMounter.ScanDir, + PlatformId: conf.PlatformId, + Options: conf.Options, + Runtime: "aws-ebs", + }, asset) + if err != nil { + c.Close() + return nil, err + } + c.volumeMounter = volumeMounter + c.FsProvider = fsConn + asset.IdDetector = []string{ids.IdDetector_Hostname} + var ok bool + asset.Platform, ok = detector.DetectOS(fsConn) + if !ok { + c.Close() + return nil, errors.New("failed to detect OS") + } + asset.Id = conf.Type + asset.Platform.Runtime = c.Runtime() + return c, nil +} + +func (c *AwsEbsConnection) FileInfo(path string) (shared.FileInfoDetails, error) { + return shared.FileInfoDetails{}, errors.New("FileInfo not implemented") +} + +func (c *AwsEbsConnection) FileSystem() afero.Fs { + return c.FsProvider.FileSystem() +} + +func (c *AwsEbsConnection) Close() { + log.Debug().Msg("close aws ebs connection") + if c.opts != nil { + if c.opts[snapshot.NoSetup] == "true" { + return + } + } + ctx := context.Background() + if c.volumeMounter != nil { + err := c.volumeMounter.UnmountVolumeFromInstance() + if err != nil { + log.Error().Err(err).Msg("unable to unmount volume") + } + err = c.DetachVolumeFromInstance(ctx, c.scanVolumeInfo) + if err != nil { + log.Error().Err(err).Msg("unable to detach volume") + } + err = c.volumeMounter.RemoveTempScanDir() + if err != nil { + log.Error().Err(err).Msg("unable to remove dir") + } + } + // only delete the volume if we created it, e.g., if we're scanning a snapshot + if val, ok := c.scanVolumeInfo.Tags["createdBy"]; ok { + if val == "Mondoo" { + err := c.DeleteCreatedVolume(ctx, c.scanVolumeInfo) + if err != nil { + log.Error().Err(err).Msg("unable to delete volume") + } + log.Info().Str("vol-id", c.scanVolumeInfo.Id).Msg("deleted temporary volume created by Mondoo") + } + } else { + log.Debug().Str("vol-id", c.scanVolumeInfo.Id).Msg("skipping volume deletion, not created by Mondoo") + } +} + +func RawInstanceInfo(cfg aws.Config) (*imds.InstanceIdentityDocument, error) { + metadata := imds.NewFromConfig(cfg) + ctx := context.Background() + doc, err := metadata.GetInstanceIdentityDocument(ctx, &imds.GetInstanceIdentityDocumentInput{}) + if err != nil { + return nil, err + } + return &doc.InstanceIdentityDocument, nil +} + +func (c *AwsEbsConnection) Identifier() (string, error) { + return c.target.PlatformId, nil +} + +func GetRawInstanceInfo(profile string) (*imds.InstanceIdentityDocument, error) { + ctx := context.Background() + var cfg aws.Config + var err error + if profile == "" { + cfg, err = config.LoadDefaultConfig(ctx) + } else { + cfg, err = config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(profile)) + } + if err != nil { + return nil, errors.Wrap(err, "could not load aws configuration") + } + i, err := RawInstanceInfo(cfg) + if err != nil { + return nil, errors.Wrap(err, "could not load instance info: aws-ec2-ebs provider is only valid on ec2 instances") + } + return i, nil +} + +func NewInstanceId(account string, region string, id string) (*awsec2ebstypes.InstanceId, error) { + if region == "" || id == "" || account == "" { + return nil, errors.New("invalid instance id. account, region and instance id required.") + } + return &awsec2ebstypes.InstanceId{Account: account, Region: region, Id: id}, nil +} + +func ParseInstanceId(path string) (*awsec2ebstypes.InstanceId, error) { + if !IsValidInstanceId(path) { + return nil, errors.New("invalid instance id. expected account//region//instance/") + } + keyValues := strings.Split(path, "/") + if len(keyValues) != 6 { + return nil, errors.New("invalid instance id. expected account//region//instance/") + } + return NewInstanceId(keyValues[1], keyValues[3], keyValues[5]) +} + +var VALID_INSTANCE_ID = regexp.MustCompile(`^account/\d{12}/region\/(us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d\/instance\/.+$`) + +func IsValidInstanceId(path string) bool { + return VALID_INSTANCE_ID.MatchString(path) +} + +func resourceTags(resourceType types.ResourceType, instanceId string) []types.TagSpecification { + return []types.TagSpecification{ + { + ResourceType: resourceType, + Tags: []types.Tag{ + {Key: aws.String("createdBy"), Value: aws.String("Mondoo")}, + {Key: aws.String("Created By"), Value: aws.String("Mondoo")}, + {Key: aws.String("Created From Instance"), Value: aws.String(instanceId)}, + }, + }, + } +} + +func ParseEbsTransportUrl(path string) (*awsec2ebstypes.EbsTransportTarget, error) { + keyValues := strings.Split(path, "/") + if len(keyValues) != 6 { + return nil, errors.New("invalid id. expected account//region//{instance, volume, or snapshot}/") + } + + var itemType string + switch keyValues[4] { + case "volume": + itemType = awsec2ebstypes.EBSTargetVolume + case "snapshot": + itemType = awsec2ebstypes.EBSTargetSnapshot + default: + itemType = awsec2ebstypes.EBSTargetInstance + } + + return &awsec2ebstypes.EbsTransportTarget{Account: keyValues[1], Region: keyValues[3], Id: keyValues[5], Type: itemType}, nil +} + +func (c *AwsEbsConnection) Name() string { + return "aws ebs" +} + +func (c *AwsEbsConnection) ID() uint32 { + return c.id +} + +func (c *AwsEbsConnection) Asset() *inventory.Asset { + return c.asset +} + +func (c *AwsEbsConnection) Capabilities() shared.Capabilities { + return shared.Capability_RunCommand // not true, update to nothing +} + +func (c *AwsEbsConnection) RunCommand(command string) (*shared.Command, error) { + return nil, errors.New("unimplemented") +} + +func (c *AwsEbsConnection) Type() shared.ConnectionType { + return EBSConnectionType +} + +func (c *AwsEbsConnection) Runtime() string { + return "aws-ebs" +} + +func (c *AwsEbsConnection) PlatformInfo() *inventory.Platform { + return &inventory.Platform{ + Name: "aws-ebs", + Title: "aws-ebs", + Runtime: c.Runtime(), + } +} diff --git a/providers/aws/connection/awsec2ebsconn/setup.go b/providers/aws/connection/awsec2ebsconn/setup.go new file mode 100644 index 0000000000..c023c527e9 --- /dev/null +++ b/providers/aws/connection/awsec2ebsconn/setup.go @@ -0,0 +1,445 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package awsec2ebsconn + +import ( + "context" + "math/rand" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/smithy-go" + "github.com/cockroachdb/errors" + "github.com/rs/zerolog/log" + awsec2ebstypes "go.mondoo.com/cnquery/providers/aws/connection/awsec2ebsconn/types" +) + +func (c *AwsEbsConnection) Validate(ctx context.Context) (*types.Instance, *awsec2ebstypes.VolumeInfo, *awsec2ebstypes.SnapshotId, error) { + target := c.target + switch c.targetType { + case awsec2ebstypes.EBSTargetInstance: + log.Info().Interface("instance", target).Msg("validate state") + resp, err := c.targetRegionEc2svc.DescribeInstances(ctx, &ec2.DescribeInstancesInput{InstanceIds: []string{target.Id}}) + if err != nil { + return nil, nil, nil, err + } + if !InstanceIsInRunningOrStoppedState(resp.Reservations[0].Instances[0].State) { + return nil, nil, nil, errors.New("instance must be in running or stopped state") + } + return &resp.Reservations[0].Instances[0], nil, nil, nil + case awsec2ebstypes.EBSTargetVolume: + log.Info().Interface("volume", target).Msg("validate exists") + vols, err := c.targetRegionEc2svc.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{VolumeIds: []string{target.Id}}) + if err != nil { + return nil, nil, nil, err + } + if len(vols.Volumes) > 0 { + vol := vols.Volumes[0] + if vol.State != types.VolumeStateAvailable { + // we can still scan it, it just means we have to do the whole snapshot/create volume dance + log.Warn().Msg("volume specified is not in available state") + return nil, &awsec2ebstypes.VolumeInfo{Id: c.target.Id, Account: c.target.AccountId, Region: c.target.Region, IsAvailable: false, Tags: awsTagsToMap(vol.Tags)}, nil, nil + } + return nil, &awsec2ebstypes.VolumeInfo{Id: c.target.Id, Account: c.target.AccountId, Region: c.target.Region, IsAvailable: true, Tags: awsTagsToMap(vol.Tags)}, nil, nil + } + case awsec2ebstypes.EBSTargetSnapshot: + log.Info().Interface("snapshot", target).Msg("validate exists") + snaps, err := c.targetRegionEc2svc.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{SnapshotIds: []string{target.Id}}) + if err != nil { + return nil, nil, nil, err + } + if len(snaps.Snapshots) > 0 { + return nil, nil, &awsec2ebstypes.SnapshotId{Id: c.target.Id, Account: c.target.AccountId, Region: c.target.Region}, nil + } + default: + return nil, nil, nil, errors.New("cannot validate; unrecognized ebs target") + } + return nil, nil, nil, errors.New("cannot validate; unrecognized ebs target") +} + +func (c *AwsEbsConnection) SetupForTargetVolume(ctx context.Context, volume awsec2ebstypes.VolumeInfo) (bool, string, error) { + log.Debug().Interface("volume", volume).Msg("setup for target volume") + if !volume.IsAvailable { + return c.SetupForTargetVolumeUnavailable(ctx, volume) + } + c.scanVolumeInfo = &volume + return c.AttachVolumeToInstance(ctx, volume) +} + +func (c *AwsEbsConnection) SetupForTargetVolumeUnavailable(ctx context.Context, volume awsec2ebstypes.VolumeInfo) (bool, string, error) { + found, snapId, err := c.FindRecentSnapshotForVolume(ctx, volume) + if err != nil { + // only log the error here, this is not a blocker + log.Error().Err(err).Msg("unable to find recent snapshot for volume") + } + if !found { + snapId, err = c.CreateSnapshotFromVolume(ctx, volume) + if err != nil { + return false, "", err + } + } + snapId, err = c.CopySnapshotToRegion(ctx, snapId) + if err != nil { + return false, "", err + } + volId, err := c.CreateVolumeFromSnapshot(ctx, snapId) + if err != nil { + return false, "", err + } + c.scanVolumeInfo = &volId + return c.AttachVolumeToInstance(ctx, volId) +} + +func (c *AwsEbsConnection) SetupForTargetSnapshot(ctx context.Context, snapshot awsec2ebstypes.SnapshotId) (bool, string, error) { + log.Debug().Interface("snapshot", snapshot).Msg("setup for target snapshot") + snapId, err := c.CopySnapshotToRegion(ctx, snapshot) + if err != nil { + return false, "", err + } + volId, err := c.CreateVolumeFromSnapshot(ctx, snapId) + if err != nil { + return false, "", err + } + c.scanVolumeInfo = &volId + return c.AttachVolumeToInstance(ctx, volId) +} + +func (c *AwsEbsConnection) SetupForTargetInstance(ctx context.Context, instanceinfo *types.Instance) (bool, string, error) { + log.Debug().Str("instance id", *instanceinfo.InstanceId).Msg("setup for target instance") + var err error + v, err := c.GetVolumeInfoForInstance(ctx, instanceinfo) + if err != nil { + return false, "", err + } + found, snapId, err := c.FindRecentSnapshotForVolume(ctx, v) + if err != nil { + // only log the error here, this is not a blocker + log.Error().Err(err).Msg("unable to find recent snapshot for volume") + } + if !found { + snapId, err = c.CreateSnapshotFromVolume(ctx, v) + if err != nil { + return false, "", err + } + } + snapId, err = c.CopySnapshotToRegion(ctx, snapId) + if err != nil { + return false, "", err + } + volId, err := c.CreateVolumeFromSnapshot(ctx, snapId) + if err != nil { + return false, "", err + } + c.scanVolumeInfo = &volId + return c.AttachVolumeToInstance(ctx, volId) +} + +func (c *AwsEbsConnection) GetVolumeInfoForInstance(ctx context.Context, instanceinfo *types.Instance) (awsec2ebstypes.VolumeInfo, error) { + i := c.target + log.Info().Interface("instance", i).Msg("find volume id") + + if volID := GetVolumeInfoForInstance(instanceinfo); volID != nil { + return awsec2ebstypes.VolumeInfo{Id: *volID, Region: i.Region, Account: i.AccountId, Tags: map[string]string{}}, nil + } + return awsec2ebstypes.VolumeInfo{}, errors.New("no volume id found for instance") +} + +func GetVolumeInfoForInstance(instanceinfo *types.Instance) *string { + if len(instanceinfo.BlockDeviceMappings) == 1 { + return instanceinfo.BlockDeviceMappings[0].Ebs.VolumeId + } + if len(instanceinfo.BlockDeviceMappings) > 1 { + for bi := range instanceinfo.BlockDeviceMappings { + log.Info().Interface("device", *instanceinfo.BlockDeviceMappings[bi].DeviceName).Msg("found instance block devices") + // todo: revisit this. this works for the standard ec2 instance setup, but no guarantees outside of that.. + if strings.Contains(*instanceinfo.BlockDeviceMappings[bi].DeviceName, "xvda") { // xvda is the root volume + return instanceinfo.BlockDeviceMappings[bi].Ebs.VolumeId + } + if strings.Contains(*instanceinfo.BlockDeviceMappings[bi].DeviceName, "sda1") { + return instanceinfo.BlockDeviceMappings[bi].Ebs.VolumeId + } + } + } + return nil +} + +func (c *AwsEbsConnection) FindRecentSnapshotForVolume(ctx context.Context, v awsec2ebstypes.VolumeInfo) (bool, awsec2ebstypes.SnapshotId, error) { + return FindRecentSnapshotForVolume(ctx, v, c.scannerRegionEc2svc) +} + +func FindRecentSnapshotForVolume(ctx context.Context, v awsec2ebstypes.VolumeInfo, svc *ec2.Client) (bool, awsec2ebstypes.SnapshotId, error) { + log.Info().Msg("find recent snapshot") + res, err := svc.DescribeSnapshots(ctx, + &ec2.DescribeSnapshotsInput{Filters: []types.Filter{ + {Name: aws.String("volume-id"), Values: []string{v.Id}}, + }}) + if err != nil { + return false, awsec2ebstypes.SnapshotId{}, err + } + + eighthrsago := time.Now().Add(-8 * time.Hour) + for i := range res.Snapshots { + // check the start time on all the snapshots + snapshot := res.Snapshots[i] + if snapshot.StartTime.After(eighthrsago) { + s := awsec2ebstypes.SnapshotId{Account: v.Account, Region: v.Region, Id: *snapshot.SnapshotId} + log.Info().Interface("snapshot", s).Msg("found snapshot") + snapState := snapshot.State + timeout := 0 + for snapState != types.SnapshotStateCompleted { + log.Info().Interface("state", snapState).Msg("waiting for snapshot copy completion; sleeping 10 seconds") + time.Sleep(10 * time.Second) + snaps, err := svc.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{SnapshotIds: []string{s.Id}}) + if err != nil { + var ae smithy.APIError + if errors.As(err, &ae) { + if ae.ErrorCode() == "InvalidSnapshot.NotFound" { + return false, awsec2ebstypes.SnapshotId{}, nil + } + } + return false, awsec2ebstypes.SnapshotId{}, err + } + snapState = snaps.Snapshots[0].State + if timeout == 6 { // we've waited a minute + return false, awsec2ebstypes.SnapshotId{}, errors.New("timed out waiting for recent snapshot to complete") + } + timeout++ + } + return true, s, nil + } + } + return false, awsec2ebstypes.SnapshotId{}, nil +} + +func (c *AwsEbsConnection) CreateSnapshotFromVolume(ctx context.Context, v awsec2ebstypes.VolumeInfo) (awsec2ebstypes.SnapshotId, error) { + log.Info().Msg("create snapshot") + // snapshot the volume + // use region from volume for aws config + cfgCopy := c.config.Copy() + cfgCopy.Region = v.Region + snapId, err := CreateSnapshotFromVolume(ctx, cfgCopy, v.Id, resourceTags(types.ResourceTypeSnapshot, c.target.Id)) + if err != nil { + return awsec2ebstypes.SnapshotId{}, err + } + + return awsec2ebstypes.SnapshotId{Id: *snapId, Region: v.Region, Account: v.Account}, nil +} + +func CreateSnapshotFromVolume(ctx context.Context, cfg aws.Config, volID string, tags []types.TagSpecification) (*string, error) { + ec2svc := ec2.NewFromConfig(cfg) + res, err := ec2svc.CreateSnapshot(ctx, &ec2.CreateSnapshotInput{VolumeId: &volID, TagSpecifications: tags}) + if err != nil { + return nil, err + } + + /* + NOTE re: encrypted snapshots + Snapshots that are taken from encrypted volumes are + automatically encrypted/decrypted. Volumes that are created from encrypted snapshots are + also automatically encrypted/decrypted. + */ + + // wait for snapshot to be ready + time.Sleep(10 * time.Second) + snapProgress := *res.Progress + snapState := res.State + timeout := 0 + notFoundTimeout := 0 + for snapState != types.SnapshotStateCompleted || !strings.Contains(snapProgress, "100") { + log.Info().Str("progress", snapProgress).Msg("waiting for snapshot completion; sleeping 10 seconds") + time.Sleep(10 * time.Second) + snaps, err := ec2svc.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{SnapshotIds: []string{*res.SnapshotId}}) + if err != nil { + var ae smithy.APIError + if errors.As(err, &ae) { + if ae.ErrorCode() == "InvalidSnapshot.NotFound" { + time.Sleep(30 * time.Second) // if it says it doesn't exist, even though we just created it, then it must still be busy creating + notFoundTimeout++ + if notFoundTimeout > 10 { + return nil, errors.New("timed out wating for created snapshot to complete; snapshot not found") + } + continue + } + } + return nil, err + } + if len(snaps.Snapshots) != 1 { + return nil, errors.Newf("expected one snapshot, got %d", len(snaps.Snapshots)) + } + snapProgress = *snaps.Snapshots[0].Progress + snapState = snaps.Snapshots[0].State + if timeout > 24 { // 4 minutes + return nil, errors.New("timed out wating for created snapshot to complete") + } + } + log.Info().Str("progress", snapProgress).Msg("snapshot complete") + + return res.SnapshotId, nil +} + +func (c *AwsEbsConnection) CopySnapshotToRegion(ctx context.Context, snapshot awsec2ebstypes.SnapshotId) (awsec2ebstypes.SnapshotId, error) { + log.Info().Str("snapshot", snapshot.Region).Str("scanner instance", c.scannerInstance.Region).Msg("checking snapshot region") + if snapshot.Region == c.scannerInstance.Region { + // we only need to copy the snapshot to the scanner region if it is not already in the same region + return snapshot, nil + } + var newSnapshot awsec2ebstypes.SnapshotId + log.Info().Msg("copy snapshot") + // snapshot the volume + res, err := c.scannerRegionEc2svc.CopySnapshot(ctx, &ec2.CopySnapshotInput{SourceRegion: &snapshot.Region, SourceSnapshotId: &snapshot.Id, TagSpecifications: resourceTags(types.ResourceTypeSnapshot, c.target.Id)}) + if err != nil { + return newSnapshot, err + } + + // wait for snapshot to be ready + snaps, err := c.scannerRegionEc2svc.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{SnapshotIds: []string{*res.SnapshotId}}) + if err != nil { + return newSnapshot, err + } + snapState := snaps.Snapshots[0].State + for snapState != types.SnapshotStateCompleted { + log.Info().Interface("state", snapState).Msg("waiting for snapshot copy completion; sleeping 10 seconds") + time.Sleep(10 * time.Second) + snaps, err := c.scannerRegionEc2svc.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{SnapshotIds: []string{*res.SnapshotId}}) + if err != nil { + return newSnapshot, err + } + snapState = snaps.Snapshots[0].State + } + return awsec2ebstypes.SnapshotId{Id: *res.SnapshotId, Region: c.config.Region, Account: c.scannerInstance.Account}, nil +} + +func (c *AwsEbsConnection) CreateVolumeFromSnapshot(ctx context.Context, snapshot awsec2ebstypes.SnapshotId) (awsec2ebstypes.VolumeInfo, error) { + log.Info().Msg("create volume") + var vol awsec2ebstypes.VolumeInfo + + out, err := c.scannerRegionEc2svc.CreateVolume(ctx, &ec2.CreateVolumeInput{ + SnapshotId: &snapshot.Id, + AvailabilityZone: &c.scannerInstance.Zone, + TagSpecifications: resourceTags(types.ResourceTypeVolume, c.target.Id), + }) + if err != nil { + return vol, err + } + + /* + NOTE re: encrypted snapshots + Snapshots that are taken from encrypted volumes are + automatically encrypted/decrypted. Volumes that are created from encrypted snapshots are + also automatically encrypted/decrypted. + */ + + state := out.State + for state != types.VolumeStateAvailable { + log.Info().Interface("state", state).Msg("waiting for volume creation completion; sleeping 10 seconds") + time.Sleep(10 * time.Second) + vols, err := c.scannerRegionEc2svc.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{VolumeIds: []string{*out.VolumeId}}) + if err != nil { + return vol, err + } + state = vols.Volumes[0].State + } + return awsec2ebstypes.VolumeInfo{Id: *out.VolumeId, Region: c.config.Region, Account: c.scannerInstance.Account, Tags: awsTagsToMap(out.Tags)}, nil +} + +func newVolumeAttachmentLoc() string { + chars := []rune("bcdefghijklmnopqrstuvwxyz") // a is reserved for the root volume + randomIndex := rand.Intn(len(chars)) + c := chars[randomIndex] + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html + return "/dev/sd" + string(c) +} + +func AttachVolume(ctx context.Context, ec2svc *ec2.Client, location string, volID string, instanceID string) (string, types.VolumeAttachmentState, error) { + res, err := ec2svc.AttachVolume(ctx, &ec2.AttachVolumeInput{ + Device: aws.String(location), VolumeId: &volID, + InstanceId: &instanceID, + }) + if err != nil { + log.Error().Err(err).Str("volume", volID).Msg("attach volume err") + var ae smithy.APIError + if errors.As(err, &ae) { + if ae.ErrorCode() != "InvalidParameterValue" { + // we don't want to return the err if it's invalid parameter value + return location, "", err + } + } + // if invalid, it could be something else is using that space, try to mount to diff location + newlocation := newVolumeAttachmentLoc() + if location != newlocation { + location = newlocation + } else { + location = newVolumeAttachmentLoc() // we shouldn't have gotten the same one the first go round, but it is randomized, so there is a possibility. try again in that case. + } + res, err = ec2svc.AttachVolume(ctx, &ec2.AttachVolumeInput{ + Device: aws.String(location), VolumeId: &volID, // warning: there is no guarantee that aws will place the volume at this location + InstanceId: &instanceID, + }) + if err != nil { + log.Error().Err(err).Str("volume", volID).Msg("attach volume err") + return location, "", err + } + } + if res.Device != nil { + log.Debug().Str("location", *res.Device).Msg("attached volume") + location = *res.Device + } + return location, res.State, nil +} + +func (c *AwsEbsConnection) AttachVolumeToInstance(ctx context.Context, volume awsec2ebstypes.VolumeInfo) (bool, string, error) { + log.Info().Str("volume id", volume.Id).Msg("attach volume") + location := newVolumeAttachmentLoc() + ready := false + loc, state, err := AttachVolume(ctx, c.scannerRegionEc2svc, newVolumeAttachmentLoc(), volume.Id, c.scannerInstance.Id) + if err != nil { + return ready, "", err + } + location = loc // warning: there is no guarantee from AWS that the device will be placed there + log.Debug().Str("location", location).Msg("target volume") + + /* + NOTE: re: encrypted volumes + Encrypted EBS volumes must be attached + to instances that support Amazon EBS encryption: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html + */ + + // here we have the attachment state + if state != types.VolumeAttachmentStateAttached { + var volState types.VolumeState + for volState != types.VolumeStateInUse { + time.Sleep(10 * time.Second) + resp, err := c.scannerRegionEc2svc.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{VolumeIds: []string{volume.Id}}) + if err != nil { + return ready, location, err + } + if len(resp.Volumes) == 1 { + volState = resp.Volumes[0].State + } + log.Info().Interface("state", volState).Msg("waiting for volume attachment completion") + } + } + return true, location, nil +} + +func awsTagsToMap(tags []types.Tag) map[string]string { + m := make(map[string]string) + for _, t := range tags { + if t.Key != nil && t.Value != nil { + m[*t.Key] = *t.Value + } + } + return m +} + +func InstanceIsInRunningOrStoppedState(state *types.InstanceState) bool { + // instance state 16 == running, 80 == stopped + if state == nil { + return false + } + return *state.Code == 16 || *state.Code == 80 +} diff --git a/providers/aws/connection/awsec2ebsconn/setup_test.go b/providers/aws/connection/awsec2ebsconn/setup_test.go new file mode 100644 index 0000000000..0f04be402d --- /dev/null +++ b/providers/aws/connection/awsec2ebsconn/setup_test.go @@ -0,0 +1,38 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build debugtest +// +build debugtest + +package awsec2ebsconn + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "gotest.tools/assert" +) + +func awsTestConfig() aws.Config { + cfg, err := config.LoadDefaultConfig(context.Background(), + config.WithSharedConfigProfile("mondoo-demo"), + config.WithRegion("us-east-1"), + ) + if err != nil { + panic(err) + } + + return cfg +} + +func TestFindRecentSnapshot(t *testing.T) { + ec2svc := ec2.NewFromConfig(awsTestConfig()) + e := Provider{scannerRegionEc2svc: ec2svc} + found, _ := e.FindRecentSnapshotForVolume(context.Background(), VolumeId{Id: "vol-0c04d709ea3e59096", Region: "us-east-1", Account: "185972265011"}) + assert.Equal(t, found, true) + // found, _ = e.FindRecentSnapshotForVolume(context.Background(), VolumeId{Id: "vol-0d5df63d656ac4d9c", Region: "us-east-1", Account: "185972265011"}) + // assert.Equal(t, found, true) +} diff --git a/providers/aws/connection/awsec2ebsconn/setup_unit_test.go b/providers/aws/connection/awsec2ebsconn/setup_unit_test.go new file mode 100644 index 0000000000..cb349f9399 --- /dev/null +++ b/providers/aws/connection/awsec2ebsconn/setup_unit_test.go @@ -0,0 +1,20 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package awsec2ebsconn + +import ( + "math/rand" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestNewVolumeAttachmentLoc(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + loc1 := newVolumeAttachmentLoc() + require.Equal(t, len(loc1), 8) + require.Equal(t, strings.HasPrefix(loc1, "/dev/sd"), true) +} diff --git a/providers/aws/connection/awsec2ebsconn/types/types.go b/providers/aws/connection/awsec2ebsconn/types/types.go new file mode 100644 index 0000000000..7c45302e37 --- /dev/null +++ b/providers/aws/connection/awsec2ebsconn/types/types.go @@ -0,0 +1,54 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package awsec2ebstypes + +import "path" + +const ( + EBSTargetInstance = "instance" + EBSTargetVolume = "volume" + EBSTargetSnapshot = "snapshot" +) + +type SnapshotId struct { + Id string + Region string + Account string +} + +type EbsTransportTarget struct { + Account string + Region string + Id string + Type string +} + +type TargetInfo struct { + PlatformId string + AccountId string + Region string + Id string +} + +type InstanceId struct { + Id string + Region string + Name string + Account string + Zone string + MarketplaceImg bool +} + +type VolumeInfo struct { + Id string + Region string + Account string + IsAvailable bool + Tags map[string]string +} + +func (s *InstanceId) String() string { + // e.g. account/999000999000/region/us-east-2/instance/i-0989478343232 + return path.Join("account", s.Account, "region", s.Region, "instance", s.Id) +} diff --git a/providers/aws/connection/awsec2ebsconn/types/types_test.go b/providers/aws/connection/awsec2ebsconn/types/types_test.go new file mode 100644 index 0000000000..9707a4ebd5 --- /dev/null +++ b/providers/aws/connection/awsec2ebsconn/types/types_test.go @@ -0,0 +1,19 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package awsec2ebstypes + +import ( + "testing" + + "gotest.tools/assert" +) + +func TestParseInstanceId(t *testing.T) { + path := "account/185972265011/region/us-east-1/instance/i-07f67838ada5879af" + id, err := ParseInstanceId(path) + assert.NilError(t, err) + assert.Equal(t, id.Account, "185972265011") + assert.Equal(t, id.Region, "us-east-1") + assert.Equal(t, id.Id, "i-07f67838ada5879af") +} diff --git a/providers/aws/connection/platform.go b/providers/aws/connection/platform.go index 9d21bbd097..b8be5e6306 100644 --- a/providers/aws/connection/platform.go +++ b/providers/aws/connection/platform.go @@ -14,7 +14,7 @@ func GetPlatformForObject(platformName string) *inventory.Platform { return &inventory.Platform{ Name: platformName, Title: getTitleForPlatformName(platformName), - Kind: "aws_object", + Kind: "aws-object", Runtime: "aws", } } diff --git a/providers/aws/go.mod b/providers/aws/go.mod index 94b6325abc..63eccbea1c 100644 --- a/providers/aws/go.mod +++ b/providers/aws/go.mod @@ -9,6 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.21.0 github.com/aws/aws-sdk-go-v2/config v1.18.39 github.com/aws/aws-sdk-go-v2/credentials v1.13.37 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 github.com/aws/aws-sdk-go-v2/service/accessanalyzer v1.20.5 github.com/aws/aws-sdk-go-v2/service/acm v1.18.5 github.com/aws/aws-sdk-go-v2/service/apigateway v1.18.0 @@ -55,6 +56,7 @@ require ( github.com/spf13/afero v1.9.5 github.com/stretchr/testify v1.8.4 go.mondoo.com/cnquery v0.0.0-20230915180754-c5f61bc705cf + gotest.tools v2.2.0+incompatible k8s.io/client-go v0.28.2 ) @@ -65,7 +67,6 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/StackExchange/wmi v1.2.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 // indirect @@ -98,6 +99,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-containerregistry v0.16.1 // indirect github.com/google/uuid v1.3.1 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect diff --git a/providers/aws/go.sum b/providers/aws/go.sum index a20ad37e0a..1a1629eb72 100644 --- a/providers/aws/go.sum +++ b/providers/aws/go.sum @@ -277,6 +277,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ= github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -774,6 +775,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/providers/aws/provider/provider.go b/providers/aws/provider/provider.go index af3a938c24..6aa466ab4c 100644 --- a/providers/aws/provider/provider.go +++ b/providers/aws/provider/provider.go @@ -12,6 +12,7 @@ import ( "go.mondoo.com/cnquery/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/providers-sdk/v1/upstream" "go.mondoo.com/cnquery/providers/aws/connection" + "go.mondoo.com/cnquery/providers/aws/connection/awsec2ebsconn" "go.mondoo.com/cnquery/providers/aws/resources" "go.mondoo.com/cnquery/providers/os/connection/shared" ) @@ -38,11 +39,16 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) if flags == nil { flags = map[string]*llx.Primitive{} } + opts := parseFlagsToOptions(flags) - inventoryConfig := &inventory.Config{ - Type: "aws", + // handle aws subcommands + if len(req.Args) >= 3 && req.Args[0] == "ec2" { + return &plugin.ParseCLIRes{Asset: handleAwsEc2Subcommands(req.Args, opts)}, nil } + inventoryConfig := &inventory.Config{ + Type: req.Connector, + } // discovery flags discoverTargets := []string{} if x, ok := flags["discover"]; ok && len(x.Array) != 0 { @@ -51,18 +57,32 @@ func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) discoverTargets = append(discoverTargets, entry) } } + inventoryConfig.Discover = &inventory.Discovery{Targets: discoverTargets} asset := inventory.Asset{ Connections: []*inventory.Config{inventoryConfig}, - Options: parseFlagsToOptions(flags), + Options: opts, } return &plugin.ParseCLIRes{Asset: &asset}, nil } +func handleAwsEc2Subcommands(args []string, opts map[string]string) *inventory.Asset { + asset := &inventory.Asset{} + switch args[1] { + case "instance-connect": + return resources.InstanceConnectAsset(args, opts) + case "ssm": + return resources.SSMConnectAsset(args, opts) + case "ebs": + return resources.EbsConnectAsset(args, opts) + } + return asset +} + func parseFlagsToOptions(m map[string]*llx.Primitive) map[string]string { o := make(map[string]string, 0) for k, v := range m { - if k == "profile" || k == "region" || k == "role" || k == "endpoint-url" { + if k == "profile" || k == "region" || k == "role" || k == "endpoint-url" || k == "no-setup" { if val := string(v.Value); val != "" { o[k] = string(v.Value) } @@ -75,6 +95,12 @@ func parseFlagsToOptions(m map[string]*llx.Primitive) map[string]string { // It is not necessary to implement this method. // If you want to do some cleanup, you can do it here. func (s *Service) Shutdown(req *plugin.ShutdownReq) (*plugin.ShutdownRes, error) { + for i := range s.runtimes { + runtime := s.runtimes[i] + if conn, ok := runtime.Connection.(awsec2ebsconn.AwsEbsConnection); ok { + conn.Close() + } + } return &plugin.ShutdownRes{}, nil } @@ -105,7 +131,9 @@ func (s *Service) Connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } if c, ok := conn.(*connection.AwsConnection); ok { - c.PlatformOverride = req.Asset.Platform.Name + if req.Asset.Platform != nil { + c.PlatformOverride = req.Asset.Platform.Name + } inventory, err = s.discover(c) if err != nil { return nil, err @@ -113,8 +141,8 @@ func (s *Service) Connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } return &plugin.ConnectRes{ - Id: uint32(conn.(shared.SimpleConnection).ID()), - Name: conn.(shared.SimpleConnection).Name(), + Id: uint32(conn.(shared.Connection).ID()), + Name: conn.(shared.Connection).Name(), Asset: req.Asset, Inventory: inventory, }, nil @@ -130,6 +158,10 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba var err error switch conf.Type { + case string(awsec2ebsconn.EBSConnectionType): + s.lastConnectionID++ + conn, err = awsec2ebsconn.NewAwsEbsConnection(s.lastConnectionID, conf, asset) + default: s.lastConnectionID++ conn, err = connection.NewAwsConnection(s.lastConnectionID, asset, conf) @@ -159,12 +191,19 @@ func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallba } func (s *Service) detect(asset *inventory.Asset, conn plugin.Connection) error { - c := conn.(*connection.AwsConnection) - asset.Id = c.Conf.Type + "://" + c.AccountId() - asset.Name = c.Conf.Host - asset.Platform = c.PlatformInfo() - asset.PlatformIds = []string{"//platformid.api.mondoo.app/runtime/aws/accounts" + c.AccountId()} - + if len(asset.Connections) > 0 && asset.Connections[0].Type == "ssh" { + // workaround to make sure we dont assign the aws platform to ec2 instances + return nil + } + if c, ok := conn.(*connection.AwsConnection); ok { + asset.Id = c.Conf.Type + "://" + c.AccountId() + asset.Name = c.Conf.Host + asset.Platform = c.PlatformInfo() + asset.PlatformIds = []string{"//platformid.api.mondoo.app/runtime/aws/accounts" + c.AccountId()} + } + if c, ok := conn.(*awsec2ebsconn.AwsEbsConnection); ok { + asset.Platform = c.PlatformInfo() + } return nil } diff --git a/providers/aws/resources/discovery_conversion.go b/providers/aws/resources/discovery_conversion.go index d285ca9e38..ea54d386c1 100644 --- a/providers/aws/resources/discovery_conversion.go +++ b/providers/aws/resources/discovery_conversion.go @@ -15,8 +15,11 @@ import ( "go.mondoo.com/cnquery/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/providers-sdk/v1/vault" "go.mondoo.com/cnquery/providers/aws/connection" + "go.mondoo.com/cnquery/providers/aws/connection/awsec2ebsconn" + awsec2ebstypes "go.mondoo.com/cnquery/providers/aws/connection/awsec2ebsconn/types" "go.mondoo.com/cnquery/providers/os/id/awsec2" "go.mondoo.com/cnquery/providers/os/id/containerid" + "go.mondoo.com/cnquery/providers/os/id/ids" ) type mqlObject struct { @@ -625,3 +628,95 @@ func MondooECSContainerID(containerArn string) string { } return "//platformid.api.mondoo.app/runtime/aws/ecs/v1/accounts/" + account + "/regions/" + region + "/" + id } + +func SSMConnectAsset(args []string, opts map[string]string) *inventory.Asset { + var user, id string + if len(args) == 3 { + if args[0] == "ec2" && args[1] == "ssm" { + if targets := strings.Split(args[2], "@"); len(targets) == 2 { + user = targets[0] + id = targets[1] + } + } + } + asset := &inventory.Asset{} + opts["instance"] = id + asset.IdDetector = []string{ids.IdDetector_CloudDetect} + asset.Connections = []*inventory.Config{{ + Type: "ssh", + Host: id, + Insecure: true, + Runtime: "ssh", + Credentials: []*vault.Credential{ + { + Type: vault.CredentialType_aws_ec2_ssm_session, + User: user, + }, + }, + Options: opts, + }} + return asset +} + +func InstanceConnectAsset(args []string, opts map[string]string) *inventory.Asset { + var user, id string + if len(args) == 3 { + if args[0] == "ec2" && args[1] == "instance-connect" { + if targets := strings.Split(args[2], "@"); len(targets) == 2 { + user = targets[0] + id = targets[1] + } + } + } + asset := &inventory.Asset{} + asset.IdDetector = []string{ids.IdDetector_CloudDetect} + opts["instance"] = id + asset.Connections = []*inventory.Config{{ + Type: "ssh", + Host: id, + Insecure: true, + Runtime: "ssh", + Credentials: []*vault.Credential{ + { + Type: vault.CredentialType_aws_ec2_instance_connect, + User: user, + }, + }, + Options: opts, + }} + return asset +} + +func EbsConnectAsset(args []string, opts map[string]string) *inventory.Asset { + var target, targetType string + if len(args) >= 3 { + if args[0] == "ec2" && args[1] == "ebs" { + // parse for target type: instance, volume, snapshot + switch args[2] { + case awsec2ebstypes.EBSTargetVolume: + target = args[3] + targetType = awsec2ebstypes.EBSTargetVolume + case awsec2ebstypes.EBSTargetSnapshot: + target = args[3] + targetType = awsec2ebstypes.EBSTargetSnapshot + default: + // in the case of an instance target, this is the instance id + target = args[2] + targetType = awsec2ebstypes.EBSTargetInstance + } + } + } + asset := &inventory.Asset{} + opts["type"] = targetType + opts["id"] = target + asset.Name = target + asset.IdDetector = []string{ids.IdDetector_Hostname} + asset.Connections = []*inventory.Config{{ + Type: string(awsec2ebsconn.EBSConnectionType), + Host: target, + Insecure: true, + Runtime: "aws-ebs", + Options: opts, + }} + return asset +} diff --git a/providers/os/connection/snapshot/volumemounter.go b/providers/os/connection/snapshot/volumemounter.go index 6d839eb017..a51fcb960c 100644 --- a/providers/os/connection/snapshot/volumemounter.go +++ b/providers/os/connection/snapshot/volumemounter.go @@ -15,7 +15,10 @@ import ( "github.com/rs/zerolog/log" ) -const NoSetup = "no-setup" +const ( + NoSetup = "no-setup" + IsSetup = "is-setup" +) type VolumeMounter struct { // the tmp dir we create; serves as the directory we mount the volume to