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 }