Repository: google/cloud-forensics-utils

Issue:
Starting a forensic VM outside of AWS region us-east-2 errors out
We create an EC2 instance of the analysis VM based on the below AMI.
https://github.com/google/cloud-forensics-utils/blob/40c7c86701413b2d2efc48734bf35279e8a18149/libcloudforensics/providers/aws/internal/common.py#L29

This AMI seems not to exist anymore.

Discussion:

Patch:
diff --git a/examples/aws_cli.py b/examples/aws_cli.py
index 3df2c2d..cf0f704 100644
--- a/examples/aws_cli.py
+++ b/examples/aws_cli.py
@@ -147,3 +147,19 @@ def StartAnalysisVm(args: 'argparse.Namespace') -> None:
   print('Name: {0:s}, Started: {1:s}, Region: {2:s}'.format(vm[0].name,
                                                             str(vm[1]),
                                                             vm[0].region))
+
+def ListImages(args: 'argparse.Namespace') -> None:
+  """List AMI images and filter on AMI image 'name'.
+
+  Args:
+    args (argparse.Namespace): Arguments from ArgumentParser.
+  """
+  aws_account = account.AWSAccount(args.zone)
+
+  qfilter = [{'Name':'name', 'Values':[args.filter]}]
+
+  images = aws_account.ListImages(qfilter)
+
+  for image in images:
+    print('Name: {0:s}, ImageId: {1:s}'.format(image['Name'],
+                                               image['ImageId']))
diff --git a/examples/cli.py b/examples/cli.py
index 0dd3882..df24b2e 100644
--- a/examples/cli.py
+++ b/examples/cli.py
@@ -19,12 +19,13 @@
 import argparse
 import sys
 
-from typing import Tuple, List, Union, Optional
+from typing import Tuple, List, Optional
 from examples import aws_cli, gcp_cli
 
 PROVIDER_TO_FUNC = {
     'aws': {
         'copydisk': aws_cli.CreateVolumeCopy,
+        'listimages': aws_cli.ListImages,
         'listinstances': aws_cli.ListInstances,
         'listdisks': aws_cli.ListVolumes,
         'querylogs': aws_cli.QueryLogs,
@@ -134,6 +135,10 @@ def Main() -> None:
                 ('--attach_volumes', 'Comma seperated list of volume IDs '
                                      'to attach. Maximum of 11.', None)
             ])
+  AddParser('aws', aws_subparsers, 'listimages', 'List AMI images.',
+            args=[
+                ('--filter', 'Filter to apply to Name of AMI image.', None),
+            ])
 
   # GCP parser options
   gcp_parser.add_argument('project', help='Source GCP project.')
diff --git a/libcloudforensics/providers/aws/internal/account.py b/libcloudforensics/providers/aws/internal/account.py
index 9d1b598..244a2f8 100644
--- a/libcloudforensics/providers/aws/internal/account.py
+++ b/libcloudforensics/providers/aws/internal/account.py
@@ -744,3 +744,28 @@ class AWSAccount:
     # pylint: enable=line-too-long
     block_device_mapping['Ebs']['VolumeSize'] = boot_volume_size
     return block_device_mapping
+
+  # pylint: disable=line-too-long
+  def ListImages(self,
+                 qfilter: Optional[List[Dict[str, Any]]]) -> List[Dict[str, Any]]:
+    """List AMI images.
+
+    Args:
+      qfilter (List[Dict]): The filter expression.
+      See https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_images  # pylint: disable=line-too-long
+
+    Returns:
+      List[Dict[str, Any]]: The list of images with their properties.
+
+    Raises:
+      RuntimeError: If the images could not be listed.
+    """
+
+    client = self.ClientApi(common.EC2_SERVICE)
+    try:
+      images = client.describe_images(Filters=qfilter) # type: Dict[str, List[Dict[str, Any]]]
+    except client.exceptions.ClientError as exception:
+      raise RuntimeError(str(exception))
+
+    return images['Images']
+  # pylint: enable=line-too-long


Test patch:
diff --git a/tests/providers/aws/aws_e2e.py b/tests/providers/aws/aws_e2e.py
index c30b63f..922d8cf 100644
--- a/tests/providers/aws/aws_e2e.py
+++ b/tests/providers/aws/aws_e2e.py
@@ -116,6 +116,20 @@ class EndToEndTest(unittest.TestCase):
     self.assertEqual(aws_volume.volume_id, volume_copy.volume_id)
     self._StoreVolumeForCleanup(aws_account, aws_volume)
 
+  @typing.no_type_check
+  def testListImages(self):
+    """End to end test on AWS.
+
+    Test listing AMI images with a filter.
+    """
+
+    aws_account = account.AWSAccount(self.zone)
+    qfilter = [{'Name':'name', 'Values':['Ubuntu 18.04*']}]
+    images = aws_account.ListImages(qfilter)
+
+    self.assertGreater(len(images), 0)
+    self.assertIn('Name', images[0])
+
   @typing.no_type_check
   def testEncryptedVolumeCopy(self):
     """End to end test on AWS.
diff --git a/tests/providers/aws/aws_test.py b/tests/providers/aws/aws_test.py
index ee9656f..b81291c 100644
--- a/tests/providers/aws/aws_test.py
+++ b/tests/providers/aws/aws_test.py
@@ -96,6 +96,13 @@ MOCK_DESCRIBE_INSTANCES_TAGS = {
     }]
 }
 
+MOCK_DESCRIBE_IMAGES = {
+    'Images' : [
+        {'Name': 'Ubuntu 18.04 LTS', 'Public': True},
+        {'Name': 'Ubuntu 18.04 with GUI', 'Public': False}
+    ]
+}
+
 MOCK_DESCRIBE_VOLUMES = {
     'Volumes': [{
         'VolumeId': FAKE_VOLUME.volume_id,
@@ -425,6 +432,15 @@ class AWSAccountTest(unittest.TestCase):
     # pylint: enable=protected-access
     self.assertEqual(50, config['Ebs']['VolumeSize'])
 
+  @typing.no_type_check
+  @mock.patch('libcloudforensics.providers.aws.internal.account.AWSAccount.ClientApi')
+  def testListImages(self, mock_ec2_api):
+    """Test that AMI images are correctly listed."""
+    describe_images = mock_ec2_api.return_value.describe_images
+    describe_images.return_value = MOCK_DESCRIBE_IMAGES
+    images = FAKE_AWS_ACCOUNT.ListImages(qfilter=None)
+    self.assertEqual(2, len(images))
+    self.assertIn('Name', images[0])
 
 class AWSInstanceTest(unittest.TestCase):
   """Test AWSInstance class."""