go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/_motor/providers/awsec2ebs/setup.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package awsec2ebs
     5  
     6  import (
     7  	"context"
     8  	"math/rand"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/aws/aws-sdk-go-v2/aws"
    13  	"github.com/aws/aws-sdk-go-v2/service/ec2"
    14  	"github.com/aws/aws-sdk-go-v2/service/ec2/types"
    15  	"github.com/aws/smithy-go"
    16  	"github.com/cockroachdb/errors"
    17  	"github.com/rs/zerolog/log"
    18  	motoraws "go.mondoo.com/cnquery/motor/discovery/aws"
    19  )
    20  
    21  func (t *Provider) Validate(ctx context.Context) (*types.Instance, *VolumeInfo, *SnapshotId, error) {
    22  	target := t.target
    23  	switch t.targetType {
    24  	case EBSTargetInstance:
    25  		log.Info().Interface("instance", target).Msg("validate state")
    26  		resp, err := t.targetRegionEc2svc.DescribeInstances(ctx, &ec2.DescribeInstancesInput{InstanceIds: []string{target.Id}})
    27  		if err != nil {
    28  			return nil, nil, nil, err
    29  		}
    30  		if !motoraws.InstanceIsInRunningOrStoppedState(resp.Reservations[0].Instances[0].State) {
    31  			return nil, nil, nil, errors.New("instance must be in running or stopped state")
    32  		}
    33  		return &resp.Reservations[0].Instances[0], nil, nil, nil
    34  	case EBSTargetVolume:
    35  		log.Info().Interface("volume", target).Msg("validate exists")
    36  		vols, err := t.targetRegionEc2svc.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{VolumeIds: []string{target.Id}})
    37  		if err != nil {
    38  			return nil, nil, nil, err
    39  		}
    40  		if len(vols.Volumes) > 0 {
    41  			vol := vols.Volumes[0]
    42  			if vol.State != types.VolumeStateAvailable {
    43  				// we can still scan it, it just means we have to do the whole snapshot/create volume dance
    44  				log.Warn().Msg("volume specified is not in available state")
    45  				return nil, &VolumeInfo{Id: t.target.Id, Account: t.target.AccountId, Region: t.target.Region, IsAvailable: false, Tags: awsTagsToMap(vol.Tags)}, nil, nil
    46  			}
    47  			return nil, &VolumeInfo{Id: t.target.Id, Account: t.target.AccountId, Region: t.target.Region, IsAvailable: true, Tags: awsTagsToMap(vol.Tags)}, nil, nil
    48  		}
    49  	case EBSTargetSnapshot:
    50  		log.Info().Interface("snapshot", target).Msg("validate exists")
    51  		snaps, err := t.targetRegionEc2svc.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{SnapshotIds: []string{target.Id}})
    52  		if err != nil {
    53  			return nil, nil, nil, err
    54  		}
    55  		if len(snaps.Snapshots) > 0 {
    56  			return nil, nil, &SnapshotId{Id: t.target.Id, Account: t.target.AccountId, Region: t.target.Region}, nil
    57  		}
    58  	default:
    59  		return nil, nil, nil, errors.New("cannot validate; unrecognized ebs target")
    60  	}
    61  	return nil, nil, nil, errors.New("cannot validate; unrecognized ebs target")
    62  }
    63  
    64  func (t *Provider) SetupForTargetVolume(ctx context.Context, volume VolumeInfo) (bool, error) {
    65  	log.Debug().Interface("volume", volume).Msg("setup for target volume")
    66  	if !volume.IsAvailable {
    67  		return t.SetupForTargetVolumeUnavailable(ctx, volume)
    68  	}
    69  	t.scanVolumeInfo = &volume
    70  	return t.AttachVolumeToInstance(ctx, volume)
    71  }
    72  
    73  func (t *Provider) SetupForTargetVolumeUnavailable(ctx context.Context, volume VolumeInfo) (bool, error) {
    74  	found, snapId, err := t.FindRecentSnapshotForVolume(ctx, volume)
    75  	if err != nil {
    76  		// only log the error here, this is not a blocker
    77  		log.Error().Err(err).Msg("unable to find recent snapshot for volume")
    78  	}
    79  	if !found {
    80  		snapId, err = t.CreateSnapshotFromVolume(ctx, volume)
    81  		if err != nil {
    82  			return false, err
    83  		}
    84  	}
    85  	snapId, err = t.CopySnapshotToRegion(ctx, snapId)
    86  	if err != nil {
    87  		return false, err
    88  	}
    89  	volId, err := t.CreateVolumeFromSnapshot(ctx, snapId)
    90  	if err != nil {
    91  		return false, err
    92  	}
    93  	t.scanVolumeInfo = &volId
    94  	return t.AttachVolumeToInstance(ctx, volId)
    95  }
    96  
    97  func (t *Provider) SetupForTargetSnapshot(ctx context.Context, snapshot SnapshotId) (bool, error) {
    98  	log.Debug().Interface("snapshot", snapshot).Msg("setup for target snapshot")
    99  	snapId, err := t.CopySnapshotToRegion(ctx, snapshot)
   100  	if err != nil {
   101  		return false, err
   102  	}
   103  	volId, err := t.CreateVolumeFromSnapshot(ctx, snapId)
   104  	if err != nil {
   105  		return false, err
   106  	}
   107  	t.scanVolumeInfo = &volId
   108  	return t.AttachVolumeToInstance(ctx, volId)
   109  }
   110  
   111  func (t *Provider) SetupForTargetInstance(ctx context.Context, instanceinfo *types.Instance) (bool, error) {
   112  	log.Debug().Str("instance id", *instanceinfo.InstanceId).Msg("setup for target instance")
   113  	var err error
   114  	v, err := t.GetVolumeInfoForInstance(ctx, instanceinfo)
   115  	if err != nil {
   116  		return false, err
   117  	}
   118  	found, snapId, err := t.FindRecentSnapshotForVolume(ctx, v)
   119  	if err != nil {
   120  		// only log the error here, this is not a blocker
   121  		log.Error().Err(err).Msg("unable to find recent snapshot for volume")
   122  	}
   123  	if !found {
   124  		snapId, err = t.CreateSnapshotFromVolume(ctx, v)
   125  		if err != nil {
   126  			return false, err
   127  		}
   128  	}
   129  	snapId, err = t.CopySnapshotToRegion(ctx, snapId)
   130  	if err != nil {
   131  		return false, err
   132  	}
   133  	volId, err := t.CreateVolumeFromSnapshot(ctx, snapId)
   134  	if err != nil {
   135  		return false, err
   136  	}
   137  	t.scanVolumeInfo = &volId
   138  	return t.AttachVolumeToInstance(ctx, volId)
   139  }
   140  
   141  func (t *Provider) GetVolumeInfoForInstance(ctx context.Context, instanceinfo *types.Instance) (VolumeInfo, error) {
   142  	i := t.target
   143  	log.Info().Interface("instance", i).Msg("find volume id")
   144  
   145  	if volID := GetVolumeInfoForInstance(instanceinfo); volID != nil {
   146  		return VolumeInfo{Id: *volID, Region: i.Region, Account: i.AccountId, Tags: map[string]string{}}, nil
   147  	}
   148  	return VolumeInfo{}, errors.New("no volume id found for instance")
   149  }
   150  
   151  func GetVolumeInfoForInstance(instanceinfo *types.Instance) *string {
   152  	if len(instanceinfo.BlockDeviceMappings) == 1 {
   153  		return instanceinfo.BlockDeviceMappings[0].Ebs.VolumeId
   154  	}
   155  	if len(instanceinfo.BlockDeviceMappings) > 1 {
   156  		for bi := range instanceinfo.BlockDeviceMappings {
   157  			log.Info().Interface("device", *instanceinfo.BlockDeviceMappings[bi].DeviceName).Msg("found instance block devices")
   158  			// todo: revisit this. this works for the standard ec2 instance setup, but no guarantees outside of that..
   159  			if strings.Contains(*instanceinfo.BlockDeviceMappings[bi].DeviceName, "xvda") { // xvda is the root volume
   160  				return instanceinfo.BlockDeviceMappings[bi].Ebs.VolumeId
   161  			}
   162  			if strings.Contains(*instanceinfo.BlockDeviceMappings[bi].DeviceName, "sda1") {
   163  				return instanceinfo.BlockDeviceMappings[bi].Ebs.VolumeId
   164  			}
   165  		}
   166  	}
   167  	return nil
   168  }
   169  
   170  func (t *Provider) FindRecentSnapshotForVolume(ctx context.Context, v VolumeInfo) (bool, SnapshotId, error) {
   171  	return FindRecentSnapshotForVolume(ctx, v, t.scannerRegionEc2svc)
   172  }
   173  
   174  func FindRecentSnapshotForVolume(ctx context.Context, v VolumeInfo, svc *ec2.Client) (bool, SnapshotId, error) {
   175  	log.Info().Msg("find recent snapshot")
   176  	res, err := svc.DescribeSnapshots(ctx,
   177  		&ec2.DescribeSnapshotsInput{Filters: []types.Filter{
   178  			{Name: aws.String("volume-id"), Values: []string{v.Id}},
   179  		}})
   180  	if err != nil {
   181  		return false, SnapshotId{}, err
   182  	}
   183  
   184  	eighthrsago := time.Now().Add(-8 * time.Hour)
   185  	for i := range res.Snapshots {
   186  		// check the start time on all the snapshots
   187  		snapshot := res.Snapshots[i]
   188  		if snapshot.StartTime.After(eighthrsago) {
   189  			s := SnapshotId{Account: v.Account, Region: v.Region, Id: *snapshot.SnapshotId}
   190  			log.Info().Interface("snapshot", s).Msg("found snapshot")
   191  			snapState := snapshot.State
   192  			timeout := 0
   193  			for snapState != types.SnapshotStateCompleted {
   194  				log.Info().Interface("state", snapState).Msg("waiting for snapshot copy completion; sleeping 10 seconds")
   195  				time.Sleep(10 * time.Second)
   196  				snaps, err := svc.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{SnapshotIds: []string{s.Id}})
   197  				if err != nil {
   198  					var ae smithy.APIError
   199  					if errors.As(err, &ae) {
   200  						if ae.ErrorCode() == "InvalidSnapshot.NotFound" {
   201  							return false, SnapshotId{}, nil
   202  						}
   203  					}
   204  					return false, SnapshotId{}, err
   205  				}
   206  				snapState = snaps.Snapshots[0].State
   207  				if timeout == 6 { // we've waited a minute
   208  					return false, SnapshotId{}, errors.New("timed out waiting for recent snapshot to complete")
   209  				}
   210  				timeout++
   211  			}
   212  			return true, s, nil
   213  		}
   214  	}
   215  	return false, SnapshotId{}, nil
   216  }
   217  
   218  func (t *Provider) CreateSnapshotFromVolume(ctx context.Context, v VolumeInfo) (SnapshotId, error) {
   219  	log.Info().Msg("create snapshot")
   220  	// snapshot the volume
   221  	// use region from volume for aws config
   222  	cfgCopy := t.config.Copy()
   223  	cfgCopy.Region = v.Region
   224  	snapId, err := CreateSnapshotFromVolume(ctx, cfgCopy, v.Id, resourceTags(types.ResourceTypeSnapshot, t.target.Id))
   225  	if err != nil {
   226  		return SnapshotId{}, err
   227  	}
   228  
   229  	return SnapshotId{Id: *snapId, Region: v.Region, Account: v.Account}, nil
   230  }
   231  
   232  func CreateSnapshotFromVolume(ctx context.Context, cfg aws.Config, volID string, tags []types.TagSpecification) (*string, error) {
   233  	ec2svc := ec2.NewFromConfig(cfg)
   234  	res, err := ec2svc.CreateSnapshot(ctx, &ec2.CreateSnapshotInput{VolumeId: &volID, TagSpecifications: tags})
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  
   239  	/*
   240  		NOTE re: encrypted snapshots
   241  		Snapshots that are taken from encrypted volumes are
   242  		automatically encrypted/decrypted. Volumes that are created from encrypted snapshots are
   243  		also automatically encrypted/decrypted.
   244  	*/
   245  
   246  	// wait for snapshot to be ready
   247  	time.Sleep(10 * time.Second)
   248  	snapProgress := *res.Progress
   249  	snapState := res.State
   250  	timeout := 0
   251  	notFoundTimeout := 0
   252  	for snapState != types.SnapshotStateCompleted || !strings.Contains(snapProgress, "100") {
   253  		log.Info().Str("progress", snapProgress).Msg("waiting for snapshot completion; sleeping 10 seconds")
   254  		time.Sleep(10 * time.Second)
   255  		snaps, err := ec2svc.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{SnapshotIds: []string{*res.SnapshotId}})
   256  		if err != nil {
   257  			var ae smithy.APIError
   258  			if errors.As(err, &ae) {
   259  				if ae.ErrorCode() == "InvalidSnapshot.NotFound" {
   260  					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
   261  					notFoundTimeout++
   262  					if notFoundTimeout > 10 {
   263  						return nil, errors.New("timed out wating for created snapshot to complete; snapshot not found")
   264  					}
   265  					continue
   266  				}
   267  			}
   268  			return nil, err
   269  		}
   270  		if len(snaps.Snapshots) != 1 {
   271  			return nil, errors.Newf("expected one snapshot, got %d", len(snaps.Snapshots))
   272  		}
   273  		snapProgress = *snaps.Snapshots[0].Progress
   274  		snapState = snaps.Snapshots[0].State
   275  		if timeout > 24 { // 4 minutes
   276  			return nil, errors.New("timed out wating for created snapshot to complete")
   277  		}
   278  	}
   279  	log.Info().Str("progress", snapProgress).Msg("snapshot complete")
   280  
   281  	return res.SnapshotId, nil
   282  }
   283  
   284  func (t *Provider) CopySnapshotToRegion(ctx context.Context, snapshot SnapshotId) (SnapshotId, error) {
   285  	log.Info().Str("snapshot", snapshot.Region).Str("scanner instance", t.scannerInstance.Region).Msg("checking snapshot region")
   286  	if snapshot.Region == t.scannerInstance.Region {
   287  		// we only need to copy the snapshot to the scanner region if it is not already in the same region
   288  		return snapshot, nil
   289  	}
   290  	var newSnapshot SnapshotId
   291  	log.Info().Msg("copy snapshot")
   292  	// snapshot the volume
   293  	res, err := t.scannerRegionEc2svc.CopySnapshot(ctx, &ec2.CopySnapshotInput{SourceRegion: &snapshot.Region, SourceSnapshotId: &snapshot.Id, TagSpecifications: resourceTags(types.ResourceTypeSnapshot, t.target.Id)})
   294  	if err != nil {
   295  		return newSnapshot, err
   296  	}
   297  
   298  	// wait for snapshot to be ready
   299  	snaps, err := t.scannerRegionEc2svc.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{SnapshotIds: []string{*res.SnapshotId}})
   300  	if err != nil {
   301  		return newSnapshot, err
   302  	}
   303  	snapState := snaps.Snapshots[0].State
   304  	for snapState != types.SnapshotStateCompleted {
   305  		log.Info().Interface("state", snapState).Msg("waiting for snapshot copy completion; sleeping 10 seconds")
   306  		time.Sleep(10 * time.Second)
   307  		snaps, err := t.scannerRegionEc2svc.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{SnapshotIds: []string{*res.SnapshotId}})
   308  		if err != nil {
   309  			return newSnapshot, err
   310  		}
   311  		snapState = snaps.Snapshots[0].State
   312  	}
   313  	return SnapshotId{Id: *res.SnapshotId, Region: t.config.Region, Account: t.scannerInstance.Account}, nil
   314  }
   315  
   316  func (t *Provider) CreateVolumeFromSnapshot(ctx context.Context, snapshot SnapshotId) (VolumeInfo, error) {
   317  	log.Info().Msg("create volume")
   318  	var vol VolumeInfo
   319  
   320  	out, err := t.scannerRegionEc2svc.CreateVolume(ctx, &ec2.CreateVolumeInput{
   321  		SnapshotId:        &snapshot.Id,
   322  		AvailabilityZone:  &t.scannerInstance.Zone,
   323  		TagSpecifications: resourceTags(types.ResourceTypeVolume, t.target.Id),
   324  	})
   325  	if err != nil {
   326  		return vol, err
   327  	}
   328  
   329  	/*
   330  		NOTE re: encrypted snapshots
   331  		Snapshots that are taken from encrypted volumes are
   332  		automatically encrypted/decrypted. Volumes that are created from encrypted snapshots are
   333  		also automatically encrypted/decrypted.
   334  	*/
   335  
   336  	state := out.State
   337  	for state != types.VolumeStateAvailable {
   338  		log.Info().Interface("state", state).Msg("waiting for volume creation completion; sleeping 10 seconds")
   339  		time.Sleep(10 * time.Second)
   340  		vols, err := t.scannerRegionEc2svc.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{VolumeIds: []string{*out.VolumeId}})
   341  		if err != nil {
   342  			return vol, err
   343  		}
   344  		state = vols.Volumes[0].State
   345  	}
   346  	return VolumeInfo{Id: *out.VolumeId, Region: t.config.Region, Account: t.scannerInstance.Account, Tags: awsTagsToMap(out.Tags)}, nil
   347  }
   348  
   349  func newVolumeAttachmentLoc() string {
   350  	chars := []rune("bcdefghijklmnopqrstuvwxyz") // a is reserved for the root volume
   351  	randomIndex := rand.Intn(len(chars))
   352  	c := chars[randomIndex]
   353  	// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html
   354  	return "/dev/sd" + string(c)
   355  }
   356  
   357  func AttachVolume(ctx context.Context, ec2svc *ec2.Client, location string, volID string, instanceID string) (string, types.VolumeAttachmentState, error) {
   358  	res, err := ec2svc.AttachVolume(ctx, &ec2.AttachVolumeInput{
   359  		Device: aws.String(location), VolumeId: &volID,
   360  		InstanceId: &instanceID,
   361  	})
   362  	if err != nil {
   363  		log.Error().Err(err).Str("volume", volID).Msg("attach volume err")
   364  		var ae smithy.APIError
   365  		if errors.As(err, &ae) {
   366  			if ae.ErrorCode() != "InvalidParameterValue" {
   367  				// we don't want to return the err if it's invalid parameter value
   368  				return location, "", err
   369  			}
   370  		}
   371  		// if invalid, it could be something else is using that space, try to mount to diff location
   372  		newlocation := newVolumeAttachmentLoc()
   373  		if location != newlocation {
   374  			location = newlocation
   375  		} else {
   376  			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.
   377  		}
   378  		res, err = ec2svc.AttachVolume(ctx, &ec2.AttachVolumeInput{
   379  			Device: aws.String(location), VolumeId: &volID, // warning: there is no guarantee that aws will place the volume at this location
   380  			InstanceId: &instanceID,
   381  		})
   382  		if err != nil {
   383  			log.Error().Err(err).Str("volume", volID).Msg("attach volume err")
   384  			return location, "", err
   385  		}
   386  	}
   387  	if res.Device != nil {
   388  		log.Debug().Str("location", *res.Device).Msg("attached volume")
   389  		location = *res.Device
   390  	}
   391  	return location, res.State, nil
   392  }
   393  
   394  func (t *Provider) AttachVolumeToInstance(ctx context.Context, volume VolumeInfo) (bool, error) {
   395  	log.Info().Str("volume id", volume.Id).Msg("attach volume")
   396  	t.volumeMounter.VolumeAttachmentLoc = newVolumeAttachmentLoc()
   397  	ready := false
   398  	location, state, err := AttachVolume(ctx, t.scannerRegionEc2svc, newVolumeAttachmentLoc(), volume.Id, t.scannerInstance.Id)
   399  	if err != nil {
   400  		return ready, err
   401  	}
   402  	t.volumeMounter.VolumeAttachmentLoc = location // warning: there is no guarantee from AWS that the device will be placed there
   403  	log.Debug().Str("location", location).Msg("target volume")
   404  
   405  	/*
   406  		NOTE: re: encrypted volumes
   407  		Encrypted EBS volumes must be attached
   408  		to instances that support Amazon EBS encryption: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html
   409  	*/
   410  
   411  	// here we have the attachment state
   412  	if state != types.VolumeAttachmentStateAttached {
   413  		var volState types.VolumeState
   414  		for volState != types.VolumeStateInUse {
   415  			time.Sleep(10 * time.Second)
   416  			resp, err := t.scannerRegionEc2svc.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{VolumeIds: []string{volume.Id}})
   417  			if err != nil {
   418  				return ready, err
   419  			}
   420  			if len(resp.Volumes) == 1 {
   421  				volState = resp.Volumes[0].State
   422  			}
   423  			log.Info().Interface("state", volState).Msg("waiting for volume attachment completion")
   424  		}
   425  	}
   426  	return true, nil
   427  }
   428  
   429  func awsTagsToMap(tags []types.Tag) map[string]string {
   430  	m := make(map[string]string)
   431  	for _, t := range tags {
   432  		if t.Key != nil && t.Value != nil {
   433  			m[*t.Key] = *t.Value
   434  		}
   435  	}
   436  	return m
   437  }