From 3983eae1c6b155025163f626cd6b93e9cc222bc7 Mon Sep 17 00:00:00 2001 From: Aaron Peterson Date: Tue, 23 Jan 2024 11:20:07 -0800 Subject: [PATCH 1/6] Initial pre-processor --- turbinia/evidence.py | 53 ++++++++++ turbinia/lib/utils.py | 15 +++ turbinia/processors/aws.py | 152 ++++++++++++++++++++++++++++ turbinia/processors/google_cloud.py | 24 +---- 4 files changed, 225 insertions(+), 19 deletions(-) create mode 100644 turbinia/processors/aws.py diff --git a/turbinia/evidence.py b/turbinia/evidence.py index 56a831b28..b96a0fcfc 100644 --- a/turbinia/evidence.py +++ b/turbinia/evidence.py @@ -917,6 +917,59 @@ def _postprocess(self): self.state[EvidenceState.ATTACHED] = False +class AwsEbsDisk(Evidence): + """Evidence object for an AWS EC2 EBS Disk. + + Attributes: + project: The cloud project name this disk is associated with. + zone: The geographic zone. + disk_name: The cloud disk name. + """ + + REQUIRED_ATTRIBUTES = ['disk_name', 'project', 'zone'] + POSSIBLE_STATES = [EvidenceState.ATTACHED, EvidenceState.MOUNTED] + + def __init__( + self, project=None, zone=None, disk_name=None, mount_partition=1, *args, + **kwargs): + """Initialization for Google Cloud Disk.""" + super(GoogleCloudDisk, self).__init__(*args, **kwargs) + self.project = project + self.zone = zone + self.disk_name = disk_name + self.mount_partition = mount_partition + self.partition_paths = None + self.cloud_only = True + self.resource_tracked = True + self.resource_id = self.disk_name + self.device_path = None + + @property + def name(self): + if self._name: + return self._name + else: + return ':'.join((self.type, self.project, self.disk_name)) + + def _preprocess(self, _, required_states): + # The GoogleCloudDisk should never need to be mounted unless it has child + # evidence (GoogleCloudDiskRawEmbedded). In all other cases, the + # DiskPartition evidence will be used. In this case we're breaking the + # evidence layer isolation and having the child evidence manage the + # mounting and unmounting. + if EvidenceState.ATTACHED in required_states: + self.device_path, partition_paths = aws.PreprocessAttachDisk( + self.disk_name) + self.partition_paths = partition_paths + self.local_path = self.device_path + self.state[EvidenceState.ATTACHED] = True + + def _postprocess(self): + if self.state[EvidenceState.ATTACHED]: + aws.PostprocessDetachDisk(self.disk_name, self.device_path) + self.state[EvidenceState.ATTACHED] = False + + class GoogleCloudDisk(Evidence): """Evidence object for a Google Cloud Disk. diff --git a/turbinia/lib/utils.py b/turbinia/lib/utils.py index 36adde6c6..2912d4fb2 100644 --- a/turbinia/lib/utils.py +++ b/turbinia/lib/utils.py @@ -173,6 +173,21 @@ def get_exe_path(filename): return binary +def is_block_device(path): + """Checks path to determine whether it is a block device. + + Args: + path: String of path to check. + + Returns: + Bool indicating success. + """ + if not os.path.exists(path): + return False + mode = os.stat(path).st_mode + return stat.S_ISBLK(mode) + + def bruteforce_password_hashes( password_hashes, tmp_dir, timeout=300, extra_args=''): """Bruteforce password hashes using Hashcat or john. diff --git a/turbinia/processors/aws.py b/turbinia/processors/aws.py new file mode 100644 index 000000000..daa84db5b --- /dev/null +++ b/turbinia/processors/aws.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Google Inc. +# +# 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. +"""Evidence processor for AWS resources.""" + +import glob +import logging +import os +import time + +from libcloudforensics.providers.aws.internal import account +from libcloudforensics.providers.aws.internal import project as gcp_project +from turbinia import config +from turbinia.lib import util +from turbinia import TurbiniaException + +log = logging.getLogger('turbinia') + +RETRY_MAX = 10 +ATTACH_SLEEP_TIME = 3 +DETACH_SLEEP_TIME = 5 + +turbinia_nonexisting_disk_path = Counter( + 'turbinia_nonexisting_disk_path', + 'Total number of non existing disk paths after attempts to attach') + + +def GetLocalInstanceId(): + """Gets the instance Id of the current machine. + + Returns: + The instance Id as a string + + Raises: + TurbiniaException: If instance name cannot be determined from metadata + server. + """ + aws_account = account.AWSAccount(zone, aws_profile) + + +def PreprocessAttachDisk(disk_id): + """Attaches Google Cloud Disk to an instance. + + Args: + disk_name(str): The name of the Cloud Disk to attach. + + Returns: + (str, list(str)): a tuple consisting of the path to the 'disk' block device + and a list of paths to partition block devices. For example: + ( + '/dev/disk/by-id/google-disk0', + ['/dev/disk/by-id/google-disk0-part1', '/dev/disk/by-id/google-disk0-p2'] + ) + + Raises: + TurbiniaException: If the device is not a block device. + """ + # TODO need: awsprofile + config.LoadConfig() + aws_account = account.AWSAccount(config.TURBINIA_ZONE, aws_profile) + instance_id = GetLocalInstanceId() + instance = aws_account.ec2.GetInstanceById(instance_id) + path = f'/dev/sd-{disk_id}' + if util.is_block_device(path): + log.info(f'Disk {disk_name:s} already attached!') + # TODO need to see if partition devices are created automatically or need to + # be enumerated in other ways. + return (path, sorted(glob.glob(f'{path:s}-part*'))) + + instance.AttachVolume(aws_account.ebs.GetVolumeById(disk_id), path) + + # instance_name = GetLocalInstanceName() + # project = gcp_project.GoogleCloudProject( + # config.TURBINIA_PROJECT, default_zone=config.TURBINIA_ZONE) + # instance = project.compute.GetInstance(instance_name) + + # disk = project.compute.GetDisk(disk_name) + # log.info(f'Attaching disk {disk_name:s} to instance {instance_name:s}') + # instance.AttachDisk(disk) + + + # Make sure we have a proper block device + for _ in range(RETRY_MAX): + if util.is_block_device(path): + log.info(f'Block device {path:s} successfully attached') + break + if os.path.exists(path): + log.info(f'Block device {path:s} mode is {os.stat(path).st_mode}') + time.sleep(ATTACH_SLEEP_TIME) + + # Final sleep to allow time between API calls. + time.sleep(ATTACH_SLEEP_TIME) + + message = None + if not os.path.exists(path): + turbinia_nonexisting_disk_path.inc() + message = f'Device path {path:s} does not exist' + elif not util.is_block_device(path): + message = f'Device path {path:s} is not a block device' + if message: + log.error(message) + raise TurbiniaException(message) + + return (path, sorted(glob.glob(f'{path:s}-part*'))) + + +def PostprocessDetachDisk(disk_name, local_path): + """Detaches Google Cloud Disk from an instance. + + Args: + disk_name(str): The name of the Cloud Disk to detach. + local_path(str): The local path to the block device to detach. + """ + #TODO: can local_path be something different than the /dev/disk/by-id/google* + if local_path: + path = local_path + else: + path = f'/dev/disk/by-id/google-{disk_name:s}' + + if not util.is_block_device(path): + log.info(f'Disk {disk_name:s} already detached!') + return + + config.LoadConfig() + instance_name = GetLocalInstanceName() + project = gcp_project.GoogleCloudProject( + config.TURBINIA_PROJECT, default_zone=config.TURBINIA_ZONE) + instance = project.compute.GetInstance(instance_name) + disk = project.compute.GetDisk(disk_name) + log.info(f'Detaching disk {disk_name:s} from instance {instance_name:s}') + instance.DetachDisk(disk) + + # Make sure device is Detached + for _ in range(RETRY_MAX): + if not os.path.exists(path): + log.info(f'Block device {path:s} is no longer attached') + break + time.sleep(DETACH_SLEEP_TIME) + + # Final sleep to allow time between API calls. + time.sleep(DETACH_SLEEP_TIME) diff --git a/turbinia/processors/google_cloud.py b/turbinia/processors/google_cloud.py index 2f7c26046..bc9337b92 100644 --- a/turbinia/processors/google_cloud.py +++ b/turbinia/processors/google_cloud.py @@ -27,6 +27,7 @@ from libcloudforensics.providers.gcp.internal import project as gcp_project from prometheus_client import Counter from turbinia import config +from turbinia.lib import util from turbinia import TurbiniaException log = logging.getLogger(__name__) @@ -40,21 +41,6 @@ 'Total number of non existing disk paths after attempts to attach') -def IsBlockDevice(path): - """Checks path to determine whether it is a block device. - - Args: - path: String of path to check. - - Returns: - Bool indicating success. - """ - if not os.path.exists(path): - return False - mode = os.stat(path).st_mode - return stat.S_ISBLK(mode) - - def GetLocalInstanceName(): """Gets the instance name of the current machine. @@ -97,7 +83,7 @@ def PreprocessAttachDisk(disk_name): TurbiniaException: If the device is not a block device. """ path = f'/dev/disk/by-id/google-{disk_name:s}' - if IsBlockDevice(path): + if util.is_block_device(path): log.info(f'Disk {disk_name:s} already attached!') return (path, sorted(glob.glob(f'{path:s}-part*'))) @@ -113,7 +99,7 @@ def PreprocessAttachDisk(disk_name): # Make sure we have a proper block device for _ in range(RETRY_MAX): - if IsBlockDevice(path): + if util.is_block_device(path): log.info(f'Block device {path:s} successfully attached') break if os.path.exists(path): @@ -127,7 +113,7 @@ def PreprocessAttachDisk(disk_name): if not os.path.exists(path): turbinia_nonexisting_disk_path.inc() message = f'Device path {path:s} does not exist' - elif not IsBlockDevice(path): + elif not util.is_block_device(path): message = f'Device path {path:s} is not a block device' if message: log.error(message) @@ -149,7 +135,7 @@ def PostprocessDetachDisk(disk_name, local_path): else: path = f'/dev/disk/by-id/google-{disk_name:s}' - if not IsBlockDevice(path): + if not util.is_block_device(path): log.info(f'Disk {disk_name:s} already detached!') return From 29a8df8af415e0b3128624fe79e044169ef7af89 Mon Sep 17 00:00:00 2001 From: Aaron Peterson Date: Thu, 25 Jan 2024 16:52:12 -0800 Subject: [PATCH 2/6] Mostly code complete disk attaching --- turbinia/config/turbinia_config_tmpl.py | 3 +- turbinia/evidence.py | 12 +- turbinia/processors/aws.py | 158 +++++++++++++++++------- 3 files changed, 124 insertions(+), 49 deletions(-) diff --git a/turbinia/config/turbinia_config_tmpl.py b/turbinia/config/turbinia_config_tmpl.py index 3a6bac8fc..0b3b349f4 100644 --- a/turbinia/config/turbinia_config_tmpl.py +++ b/turbinia/config/turbinia_config_tmpl.py @@ -24,8 +24,7 @@ # separate when running with the same Cloud projects or backend servers. INSTANCE_ID = 'turbinia-instance1' -# Which Cloud provider to use. Valid options are 'Local' and 'GCP'. Use 'GCP' -# for GCP or hybrid installations, and 'Local' for local installations. +# Which Cloud provider to use. Valid options are 'Local', 'GCP' or 'AWS'. CLOUD_PROVIDER = 'Local' # Task manager only supports 'Celery'. diff --git a/turbinia/evidence.py b/turbinia/evidence.py index b96a0fcfc..273285335 100644 --- a/turbinia/evidence.py +++ b/turbinia/evidence.py @@ -42,6 +42,8 @@ config.LoadConfig() if config.CLOUD_PROVIDER.lower() == 'gcp': from turbinia.processors import google_cloud +elif config.CLOUD_PROVIDER.lower() == 'aws': + from turbinia.processors import aws log = logging.getLogger(__name__) @@ -917,7 +919,7 @@ def _postprocess(self): self.state[EvidenceState.ATTACHED] = False -class AwsEbsDisk(Evidence): +class AwsEbsVolume(Evidence): """Evidence object for an AWS EC2 EBS Disk. Attributes: @@ -926,17 +928,17 @@ class AwsEbsDisk(Evidence): disk_name: The cloud disk name. """ - REQUIRED_ATTRIBUTES = ['disk_name', 'project', 'zone'] + REQUIRED_ATTRIBUTES = ['volume_id', 'project', 'zone'] POSSIBLE_STATES = [EvidenceState.ATTACHED, EvidenceState.MOUNTED] def __init__( - self, project=None, zone=None, disk_name=None, mount_partition=1, *args, + self, project=None, zone=None, volume_id=None, mount_partition=1, *args, **kwargs): """Initialization for Google Cloud Disk.""" - super(GoogleCloudDisk, self).__init__(*args, **kwargs) + super(AwsEbsVolume, self).__init__(*args, **kwargs) self.project = project self.zone = zone - self.disk_name = disk_name + self.volume_id = volume_id self.mount_partition = mount_partition self.partition_paths = None self.cloud_only = True diff --git a/turbinia/processors/aws.py b/turbinia/processors/aws.py index daa84db5b..6a465127e 100644 --- a/turbinia/processors/aws.py +++ b/turbinia/processors/aws.py @@ -15,12 +15,15 @@ """Evidence processor for AWS resources.""" import glob +import json import logging import os +import subprocess import time +import urllib from libcloudforensics.providers.aws.internal import account -from libcloudforensics.providers.aws.internal import project as gcp_project +from prometheus_client import Counter from turbinia import config from turbinia.lib import util from turbinia import TurbiniaException @@ -36,6 +39,67 @@ 'Total number of non existing disk paths after attempts to attach') +def GetDevicePath(): + """Gets the next free block device path from the local system. + + Returns: + new_path(str|None): The new device path name if one is found, else None. + """ + path_base = '/dev/sd' + # Recommended device names are /dev/sd[f-p] as per: + # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html#available-ec2-device-names + have_path = False + for i in range(ord('f'), ord('p') + 1): + new_path = f'{path_base}{chr(i)}' + # Using `exists` instead of `is_block_device` because even if the file + # exists and isn't a block device we still won't be able to use it as a new + # device path. + if not os.path.exists(new_path): + have_path = True + break + + if have_path: + return new_path + + return None + +def CheckVolumeAttached(disk_id): + """Uses lsblk to determine if the disk is already attached. + + AWS EBS puts the volume ID in the serial number for the device: + https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-ebs-volumes.html#identify-nvme-ebs-device + + Returns: + device_name(str|None): The name of the device if it is attached, else None + + Raises: + TurbiniaException: If the output from lsblk cannot be parsed. + """ + # From testing the volume ID seems to have the dash removed from the volume ID listed + # in the AWS console. + serial_num = disk_id.replace('-', '') + command = ['lsblk', '-o', 'NAME,SERIAL', '-J'] + result = subprocess.run(command, check=True, capture_output=True, text=True) + device_name = None + + if result.returncode == 0: + try: + lsblk_results = json.loads(result.stdout) + except json.JSONDecodeError as exception: + raise TurbiniaException(f'Unable to parse output from {command}: {exception}') + + for device in lsblk_results.get('blockdevices', []): + if device.get('serial').lower() == serial_num.lower() and device.get('name'): + device_name = f'/dev/{device.get("name")}' + log.info(f'Found device {device_name} attached with serial {serial_num}') + break + else: + log.info( + f'Received non-zero exit status {result.returncode} from {command}') + + return device_name + + def GetLocalInstanceId(): """Gets the instance Id of the current machine. @@ -46,90 +110,100 @@ def GetLocalInstanceId(): TurbiniaException: If instance name cannot be determined from metadata server. """ - aws_account = account.AWSAccount(zone, aws_profile) + req = urllib.request.Request( + 'http://169.254.169.254/latest/meta-data/instance-id') + try: + instance = urllib.request.urlopen(req).read().decode('utf-8') + except urllib.error.HTTPError as exception: + raise TurbiniaException(f'Could not get instance name: {exception}') + + return instance -def PreprocessAttachDisk(disk_id): - """Attaches Google Cloud Disk to an instance. +def PreprocessAttachDisk(volume_id): + """Attaches AWS EBS volume to an instance. Args: - disk_name(str): The name of the Cloud Disk to attach. + disk_id(str): The name of volume to attach. Returns: (str, list(str)): a tuple consisting of the path to the 'disk' block device and a list of paths to partition block devices. For example: ( - '/dev/disk/by-id/google-disk0', - ['/dev/disk/by-id/google-disk0-part1', '/dev/disk/by-id/google-disk0-p2'] + '/dev/sdf', + ['/dev/sdf1', '/dev/sdf2'] ) Raises: TurbiniaException: If the device is not a block device. """ - # TODO need: awsprofile + # Check if volume is already attached + attached_device = CheckVolumeAttached(volume_id) + if attached_device: + log.info(f'Disk {volume_id} already attached as {attached_device}') + # TODO: Fix globbing for partitions + return (attached_device, sorted(glob.glob(f'{attached_device}+'))) + + # Volume is not attached so need to attach it config.LoadConfig() - aws_account = account.AWSAccount(config.TURBINIA_ZONE, aws_profile) + aws_account = account.AWSAccount(config.TURBINIA_ZONE) instance_id = GetLocalInstanceId() instance = aws_account.ec2.GetInstanceById(instance_id) - path = f'/dev/sd-{disk_id}' - if util.is_block_device(path): - log.info(f'Disk {disk_name:s} already attached!') - # TODO need to see if partition devices are created automatically or need to - # be enumerated in other ways. - return (path, sorted(glob.glob(f'{path:s}-part*'))) - - instance.AttachVolume(aws_account.ebs.GetVolumeById(disk_id), path) - - # instance_name = GetLocalInstanceName() - # project = gcp_project.GoogleCloudProject( - # config.TURBINIA_PROJECT, default_zone=config.TURBINIA_ZONE) - # instance = project.compute.GetInstance(instance_name) - - # disk = project.compute.GetDisk(disk_name) - # log.info(f'Attaching disk {disk_name:s} to instance {instance_name:s}') - # instance.AttachDisk(disk) + device_path = GetDevicePath() + instance.AttachVolume(aws_account.ebs.GetVolumeById(volume_id), device_path) # Make sure we have a proper block device for _ in range(RETRY_MAX): - if util.is_block_device(path): - log.info(f'Block device {path:s} successfully attached') + # The device path is provided in the above attach volume command but that + # name/path is not guaranted to be the actual device name that is used by + # the host so we need to check the device names again here. See here for + # more details: + # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-ebs-volumes.html#identify-nvme-ebs-device + device_path = CheckVolumeAttached(volume_id) + if device_path and util.is_block_device(device_path): + log.info(f'Block device {device_path:s} successfully attached') break - if os.path.exists(path): - log.info(f'Block device {path:s} mode is {os.stat(path).st_mode}') + if device_path and os.path.exists(device_path): + log.info( + f'Block device {device_path:s} mode is ' + f'{os.stat(device_path).st_mode}') time.sleep(ATTACH_SLEEP_TIME) # Final sleep to allow time between API calls. time.sleep(ATTACH_SLEEP_TIME) message = None - if not os.path.exists(path): + if not device_path: + message = 'No valid device paths found after attaching' + elif not os.path.exists(device_path): turbinia_nonexisting_disk_path.inc() - message = f'Device path {path:s} does not exist' - elif not util.is_block_device(path): - message = f'Device path {path:s} is not a block device' + message = f'Device path {device_path:s} does not exist' + elif not util.is_block_device(device_path): + message = f'Device path {device_path:s} is not a block device' if message: log.error(message) raise TurbiniaException(message) - return (path, sorted(glob.glob(f'{path:s}-part*'))) + # TODO: Fix globbing for partitions + return (device_path, sorted(glob.glob(f'{device_path}+'))) -def PostprocessDetachDisk(disk_name, local_path): - """Detaches Google Cloud Disk from an instance. +def PostprocessDetachDisk(volume_id, local_path): + """Detaches AWS EBS volume from an instance. Args: - disk_name(str): The name of the Cloud Disk to detach. + volume_id(str): The name of the Cloud Disk to detach. local_path(str): The local path to the block device to detach. """ #TODO: can local_path be something different than the /dev/disk/by-id/google* if local_path: path = local_path else: - path = f'/dev/disk/by-id/google-{disk_name:s}' + path = f'/dev/disk/by-id/google-{volume_id:s}' if not util.is_block_device(path): - log.info(f'Disk {disk_name:s} already detached!') + log.info(f'Disk {volume_id:s} already detached!') return config.LoadConfig() @@ -137,8 +211,8 @@ def PostprocessDetachDisk(disk_name, local_path): project = gcp_project.GoogleCloudProject( config.TURBINIA_PROJECT, default_zone=config.TURBINIA_ZONE) instance = project.compute.GetInstance(instance_name) - disk = project.compute.GetDisk(disk_name) - log.info(f'Detaching disk {disk_name:s} from instance {instance_name:s}') + disk = project.compute.GetDisk(volume_id) + log.info(f'Detaching disk {volume_id:s} from instance {instance_name:s}') instance.DetachDisk(disk) # Make sure device is Detached From 19e954a93eaa1c68725803322881a15fb7857941 Mon Sep 17 00:00:00 2001 From: Aaron Peterson Date: Tue, 30 Jan 2024 16:45:55 -0800 Subject: [PATCH 3/6] Add detach method --- turbinia/evidence.py | 18 ++++---------- turbinia/processors/aws.py | 50 ++++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/turbinia/evidence.py b/turbinia/evidence.py index 273285335..5634298fa 100644 --- a/turbinia/evidence.py +++ b/turbinia/evidence.py @@ -920,23 +920,20 @@ def _postprocess(self): class AwsEbsVolume(Evidence): - """Evidence object for an AWS EC2 EBS Disk. + """Evidence object for an AWS EBS Disk. Attributes: - project: The cloud project name this disk is associated with. zone: The geographic zone. disk_name: The cloud disk name. """ - REQUIRED_ATTRIBUTES = ['volume_id', 'project', 'zone'] + REQUIRED_ATTRIBUTES = ['volume_id', 'zone'] POSSIBLE_STATES = [EvidenceState.ATTACHED, EvidenceState.MOUNTED] def __init__( - self, project=None, zone=None, volume_id=None, mount_partition=1, *args, - **kwargs): - """Initialization for Google Cloud Disk.""" + self, zone=None, volume_id=None, mount_partition=1, *args, **kwargs): + """Initialization for AWS EBS Disk.""" super(AwsEbsVolume, self).__init__(*args, **kwargs) - self.project = project self.zone = zone self.volume_id = volume_id self.mount_partition = mount_partition @@ -951,14 +948,9 @@ def name(self): if self._name: return self._name else: - return ':'.join((self.type, self.project, self.disk_name)) + return ':'.join((self.type, self.disk_name)) def _preprocess(self, _, required_states): - # The GoogleCloudDisk should never need to be mounted unless it has child - # evidence (GoogleCloudDiskRawEmbedded). In all other cases, the - # DiskPartition evidence will be used. In this case we're breaking the - # evidence layer isolation and having the child evidence manage the - # mounting and unmounting. if EvidenceState.ATTACHED in required_states: self.device_path, partition_paths = aws.PreprocessAttachDisk( self.disk_name) diff --git a/turbinia/processors/aws.py b/turbinia/processors/aws.py index 6a465127e..e62fee3c4 100644 --- a/turbinia/processors/aws.py +++ b/turbinia/processors/aws.py @@ -63,6 +63,7 @@ def GetDevicePath(): return None + def CheckVolumeAttached(disk_id): """Uses lsblk to determine if the disk is already attached. @@ -86,12 +87,15 @@ def CheckVolumeAttached(disk_id): try: lsblk_results = json.loads(result.stdout) except json.JSONDecodeError as exception: - raise TurbiniaException(f'Unable to parse output from {command}: {exception}') + raise TurbiniaException( + f'Unable to parse output from {command}: {exception}') for device in lsblk_results.get('blockdevices', []): - if device.get('serial').lower() == serial_num.lower() and device.get('name'): + if device.get('serial').lower() == serial_num.lower() and device.get( + 'name'): device_name = f'/dev/{device.get("name")}' - log.info(f'Found device {device_name} attached with serial {serial_num}') + log.info( + f'Found device {device_name} attached with serial {serial_num}') break else: log.info( @@ -189,38 +193,36 @@ def PreprocessAttachDisk(volume_id): return (device_path, sorted(glob.glob(f'{device_path}+'))) -def PostprocessDetachDisk(volume_id, local_path): +def PostprocessDetachDisk(volume_id): """Detaches AWS EBS volume from an instance. Args: volume_id(str): The name of the Cloud Disk to detach. - local_path(str): The local path to the block device to detach. """ - #TODO: can local_path be something different than the /dev/disk/by-id/google* - if local_path: - path = local_path - else: - path = f'/dev/disk/by-id/google-{volume_id:s}' - - if not util.is_block_device(path): - log.info(f'Disk {volume_id:s} already detached!') + attached_device = CheckVolumeAttached(volume_id) + if not attached_device: + log.info(f'Disk {volume_id} no longer attached') return config.LoadConfig() - instance_name = GetLocalInstanceName() - project = gcp_project.GoogleCloudProject( - config.TURBINIA_PROJECT, default_zone=config.TURBINIA_ZONE) - instance = project.compute.GetInstance(instance_name) - disk = project.compute.GetDisk(volume_id) - log.info(f'Detaching disk {volume_id:s} from instance {instance_name:s}') - instance.DetachDisk(disk) + aws_account = account.AWSAccount(config.TURBINIA_ZONE) + instance_id = GetLocalInstanceId() + instance = aws_account.ec2.GetInstanceById(instance_id) + + log.info(f'Detaching disk {volume_id:s} from instance {instance_id:s}') + instance.DetachVolume( + aws_account.ebs.GetVolumeById(volume_id), attached_device) # Make sure device is Detached for _ in range(RETRY_MAX): - if not os.path.exists(path): - log.info(f'Block device {path:s} is no longer attached') + if not os.path.exists(attached_device): + log.info(f'Block device {attached_device:s} is no longer attached') break time.sleep(DETACH_SLEEP_TIME) - # Final sleep to allow time between API calls. - time.sleep(DETACH_SLEEP_TIME) + if os.path.exists(attached_device): + raise TurbiniaException( + f'Could not detach volume {volume_id} with device name ' + f'{attached_device}') + else: + log.info(f'Detached volume {volume_id} with device name {attached_device}') From 4aa62e07da70fc62cd265aa0a0b61bffdaea0a77 Mon Sep 17 00:00:00 2001 From: Aaron Peterson Date: Wed, 1 May 2024 14:07:48 -0700 Subject: [PATCH 4/6] Fix disk_name --- turbinia/evidence.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/turbinia/evidence.py b/turbinia/evidence.py index 5634298fa..1f73b3f97 100644 --- a/turbinia/evidence.py +++ b/turbinia/evidence.py @@ -924,43 +924,47 @@ class AwsEbsVolume(Evidence): Attributes: zone: The geographic zone. - disk_name: The cloud disk name. + volume_id: The unique volume id for the disk. + disk_name: Optional name for the disk. """ REQUIRED_ATTRIBUTES = ['volume_id', 'zone'] POSSIBLE_STATES = [EvidenceState.ATTACHED, EvidenceState.MOUNTED] def __init__( - self, zone=None, volume_id=None, mount_partition=1, *args, **kwargs): + self, zone, volume_id, disk_name=None, mount_partition=1, *args, **kwargs): """Initialization for AWS EBS Disk.""" super(AwsEbsVolume, self).__init__(*args, **kwargs) self.zone = zone self.volume_id = volume_id + self.disk_name = disk_name self.mount_partition = mount_partition self.partition_paths = None self.cloud_only = True self.resource_tracked = True - self.resource_id = self.disk_name + self.resource_id = self.volume_id self.device_path = None @property def name(self): if self._name: return self._name + elif self.disk_name: + return ':'.join((self.type, self.disk_name, self.volume_id)) else: - return ':'.join((self.type, self.disk_name)) + return ':'.join((self.type, self.volume_id)) def _preprocess(self, _, required_states): if EvidenceState.ATTACHED in required_states: self.device_path, partition_paths = aws.PreprocessAttachDisk( - self.disk_name) + self.volume_id) self.partition_paths = partition_paths self.local_path = self.device_path self.state[EvidenceState.ATTACHED] = True def _postprocess(self): if self.state[EvidenceState.ATTACHED]: - aws.PostprocessDetachDisk(self.disk_name, self.device_path) + aws.PostprocessDetachDisk(self.volume_id, self.device_path) self.state[EvidenceState.ATTACHED] = False From e8cda86be0c7e9ef9a71bb60b443ac16cfa6ad04 Mon Sep 17 00:00:00 2001 From: Aaron Peterson Date: Wed, 1 May 2024 19:34:40 -0700 Subject: [PATCH 5/6] add aws zone and fix bugs --- turbinia/config/__init__.py | 2 ++ turbinia/config/turbinia_config_tmpl.py | 15 +++++++++++---- turbinia/evidence.py | 2 +- turbinia/lib/utils.py | 1 + turbinia/processors/aws.py | 16 +++++++++++----- turbinia/processors/google_cloud.py | 1 - 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/turbinia/config/__init__.py b/turbinia/config/__init__.py index 403981fbd..d0703f253 100644 --- a/turbinia/config/__init__.py +++ b/turbinia/config/__init__.py @@ -87,6 +87,8 @@ 'GCS_OUTPUT_PATH', 'RECIPE_FILE_DIR', 'STACKDRIVER_TRACEBACK', + # AWS config + 'AWS_ZONE', # REDIS CONFIG 'REDIS_HOST', 'REDIS_PORT', diff --git a/turbinia/config/turbinia_config_tmpl.py b/turbinia/config/turbinia_config_tmpl.py index 0b3b349f4..1de131365 100644 --- a/turbinia/config/turbinia_config_tmpl.py +++ b/turbinia/config/turbinia_config_tmpl.py @@ -277,13 +277,11 @@ ################################################################################ # Google Cloud Platform (GCP) # -# Options in this section are required if the TASK_MANAGER is set to 'PSQ'. +# Options in this section are required if the CLOUD_PROVIDER is set to 'GCP'. ################################################################################ # GCP project, region and zone where Turbinia will run. Note that Turbinia does -# not currently support multi-zone operation. Even if you are running Turbinia -# in Hybrid mode (with the Server and Workers running on local machines), you -# will still need to provide these three parameters. +# not currently support multi-zone operation. TURBINIA_PROJECT = None TURBINIA_ZONE = None TURBINIA_REGION = None @@ -300,6 +298,15 @@ # Set this to True if you would like to enable Google Cloud Error Reporting. STACKDRIVER_TRACEBACK = False +################################################################################ +# Amazon Web Services (AWS) +# +# Options in this section are required if the CLOUD_PROVIDER is set to 'AWS'. +################################################################################ + +# The default AWS zone being used. +AWS_ZONE = None + ################################################################################ # Celery / Redis / Kombu # diff --git a/turbinia/evidence.py b/turbinia/evidence.py index 1f73b3f97..2c0ddebc7 100644 --- a/turbinia/evidence.py +++ b/turbinia/evidence.py @@ -964,7 +964,7 @@ def _preprocess(self, _, required_states): def _postprocess(self): if self.state[EvidenceState.ATTACHED]: - aws.PostprocessDetachDisk(self.volume_id, self.device_path) + aws.PostprocessDetachDisk(self.volume_id) self.state[EvidenceState.ATTACHED] = False diff --git a/turbinia/lib/utils.py b/turbinia/lib/utils.py index 2912d4fb2..8bb87d6cd 100644 --- a/turbinia/lib/utils.py +++ b/turbinia/lib/utils.py @@ -19,6 +19,7 @@ import logging import os import subprocess +import stat import tempfile import threading diff --git a/turbinia/processors/aws.py b/turbinia/processors/aws.py index e62fee3c4..836e0b451 100644 --- a/turbinia/processors/aws.py +++ b/turbinia/processors/aws.py @@ -25,7 +25,7 @@ from libcloudforensics.providers.aws.internal import account from prometheus_client import Counter from turbinia import config -from turbinia.lib import util +from turbinia.lib import utils from turbinia import TurbiniaException log = logging.getLogger('turbinia') @@ -139,7 +139,8 @@ def PreprocessAttachDisk(volume_id): ) Raises: - TurbiniaException: If the device is not a block device. + TurbiniaException: If the device is not a block device or the config does + not have the required variables configured. """ # Check if volume is already attached attached_device = CheckVolumeAttached(volume_id) @@ -150,7 +151,12 @@ def PreprocessAttachDisk(volume_id): # Volume is not attached so need to attach it config.LoadConfig() - aws_account = account.AWSAccount(config.TURBINIA_ZONE) + if not config.AWS_ZONE: + msg = f'AWS_ZONE must be set in configuration file in order to attach AWS disks.' + log.error(msg) + raise TurbiniaException(msg) + + aws_account = account.AWSAccount(config.AWS_ZONE) instance_id = GetLocalInstanceId() instance = aws_account.ec2.GetInstanceById(instance_id) device_path = GetDevicePath() @@ -165,7 +171,7 @@ def PreprocessAttachDisk(volume_id): # more details: # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-ebs-volumes.html#identify-nvme-ebs-device device_path = CheckVolumeAttached(volume_id) - if device_path and util.is_block_device(device_path): + if device_path and utils.is_block_device(device_path): log.info(f'Block device {device_path:s} successfully attached') break if device_path and os.path.exists(device_path): @@ -205,7 +211,7 @@ def PostprocessDetachDisk(volume_id): return config.LoadConfig() - aws_account = account.AWSAccount(config.TURBINIA_ZONE) + aws_account = account.AWSAccount(config.AWS_ZONE) instance_id = GetLocalInstanceId() instance = aws_account.ec2.GetInstanceById(instance_id) diff --git a/turbinia/processors/google_cloud.py b/turbinia/processors/google_cloud.py index bc9337b92..2be4cbc45 100644 --- a/turbinia/processors/google_cloud.py +++ b/turbinia/processors/google_cloud.py @@ -19,7 +19,6 @@ import glob import logging import os -import stat import time from six.moves import urllib From c7254cf010a4ff525f7be5c25d37461187758b5d Mon Sep 17 00:00:00 2001 From: Aaron Peterson Date: Wed, 1 May 2024 20:06:58 -0700 Subject: [PATCH 6/6] yapf --- turbinia/evidence.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/turbinia/evidence.py b/turbinia/evidence.py index 2c0ddebc7..c72567ccd 100644 --- a/turbinia/evidence.py +++ b/turbinia/evidence.py @@ -932,7 +932,8 @@ class AwsEbsVolume(Evidence): POSSIBLE_STATES = [EvidenceState.ATTACHED, EvidenceState.MOUNTED] def __init__( - self, zone, volume_id, disk_name=None, mount_partition=1, *args, **kwargs): + self, zone, volume_id, disk_name=None, mount_partition=1, *args, + **kwargs): """Initialization for AWS EBS Disk.""" super(AwsEbsVolume, self).__init__(*args, **kwargs) self.zone = zone