Skip to content

feat(aws): add new check ec2_instance_with_outdated_ami #6910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"Provider": "aws",
"CheckID": "ec2_instance_with_outdated_ami",
"CheckTitle": "Check for EC2 Instances Using Outdated AMIs",
"CheckType": [],
"ServiceName": "ec2",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:service:region:account-id:instance/resource-id",
"Severity": "high",
"ResourceType": "AwsEc2Instance",
"Description": "This check identifies EC2 instances using outdated Amazon Machine Images (AMIs) by auditing instances to gather AMI IDs, comparing them against the latest available versions, verifying suppo and security update status, and checking for deprecation.",
"Risk": "Using outdated AMIs can expose EC2 instances to security vulnerabilities, lack of support, and missing critical updates, increasing the risk of exploitation.",
"RelatedUrl": "",
"Remediation": {
"Code": {
"CLI": "aws ec2 describe-images --image-ids <ami-id>",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html",
"Terraform": ""
},
"Recommendation": {
"Text": "Regularly update your EC2 instances to use the latest AMIs to ensure they receive the latest security patches and updates.",
"Url": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from datetime import datetime, timezone
from typing import List

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.ec2.ec2_client import ec2_client


class ec2_instance_with_outdated_ami(Check):
"""Check if EC2 instances are using outdated AMIs.

This check verifies whether EC2 instances are running on outdated AMIs that have
reached their deprecation date. If an instance is using an AMI that is deprecated,
the check fails.

Attributes:
metadata: Metadata associated with the check (inherited from Check).
"""

def execute(self) -> List[Check_Report_AWS]:
"""Execute the outdated AMI check for EC2 instances.

Iterates over all EC2 instances and checks if their AMIs have been deprecated.
If an instance is using an outdated AMI, the check fails.

Returns:
List[Check_Report_AWS]: A list containing the results of the check for each instance.
"""
findings = []
for instance in ec2_client.instances:
ami = next(
(image for image in ec2_client.images if image.id == instance.image_id),
None,
)
if ami.amazon_public:
report = Check_Report_AWS(metadata=self.metadata(), resource=instance)
report.status = "PASS"
report.status_extended = (
f"EC2 Instance {instance.id} is not using an outdated AMI."
)

if ami.deprecation_time:
deprecation_datetime = datetime.strptime(
ami.deprecation_time, "%Y-%m-%dT%H:%M:%S.%fZ"
).replace(tzinfo=timezone.utc)

if deprecation_datetime < datetime.now(timezone.utc):
report.status = "FAIL"
report.status_extended = f"EC2 Instance {instance.id} is using outdated AMI {ami.id}."

findings.append(report)

return findings
40 changes: 26 additions & 14 deletions prowler/providers/aws/services/ec2/ec2_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,20 +348,30 @@ def _get_instance_user_data(self, instance):

def _describe_images(self, regional_client):
try:
for image in regional_client.describe_images(Owners=["self"])["Images"]:
arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:image/{image['ImageId']}"
if not self.audit_resources or (
is_resource_filtered(arn, self.audit_resources)
):
self.images.append(
Image(
id=image["ImageId"],
arn=arn,
name=image.get("Name", ""),
public=image.get("Public", False),
region=regional_client.region,
tags=image.get("Tags"),
)
for owner in ["self", "amazon"]:
try:
for image in regional_client.describe_images(
Owners=[owner], IncludeDeprecated=True
)["Images"]:
arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:image/{image['ImageId']}"
if not self.audit_resources or (
is_resource_filtered(arn, self.audit_resources)
):
self.images.append(
Image(
id=image["ImageId"],
arn=arn,
name=image.get("Name", ""),
public=image.get("Public", False),
region=regional_client.region,
tags=image.get("Tags"),
deprecation_time=image.get("DeprecationTime"),
amazon_public=(owner == "amazon"),
)
)
except ClientError as error:
logger.warning(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
Expand Down Expand Up @@ -744,6 +754,8 @@ class Image(BaseModel):
arn: str
name: str
public: bool
deprecation_time: Optional[str]
amazon_public: bool = False
region: str
tags: Optional[list] = []

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
from unittest import mock

import botocore
from moto import mock_aws

from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider

make_api_call = botocore.client.BaseClient._make_api_call


def mock_make_api_call(self, operation_name, kwarg):
if operation_name == "DescribeInstances":
return {
"Reservations": [
{
"Instances": [
{
"InstanceId": "i-0123456789abcdef0",
"State": {"Name": "running"},
"InstanceType": "t2.micro",
"ImageId": "ami-12345678",
"LaunchTime": "2026-11-12T11:34:56.000Z",
"PrivateDnsName": "ip-172-31-32-101.ec2.internal",
}
]
}
]
}
elif operation_name == "DescribeImages":
if "Owners" in kwarg and kwarg["Owners"] == ["amazon"]:
return {
"Images": [
{
"ImageId": "ami-12345678",
"DeprecationTime": "2050-01-01T00:00:00.000Z",
"Public": True,
}
]
}
return make_api_call(self, operation_name, kwarg)


def mock_make_api_call_private(self, operation_name, kwarg):
if operation_name == "DescribeInstances":
return {
"Reservations": [
{
"Instances": [
{
"InstanceId": "i-0123456789abcdef0",
"State": {"Name": "running"},
"InstanceType": "t2.micro",
"ImageId": "ami-12345678",
"LaunchTime": "2026-11-12T11:34:56.000Z",
"PrivateDnsName": "ip-172-31-32-101.ec2.internal",
}
]
}
]
}
elif operation_name == "DescribeImages":
return {
"Images": [
{
"ImageId": "ami-12345678",
"DeprecationTime": "2050-01-01T00:00:00.000Z",
"Public": False,
}
]
}
return make_api_call(self, operation_name, kwarg)


def mock_make_api_call_outdated_ami(self, operation_name, kwarg):
if operation_name == "DescribeInstances":
return {
"Reservations": [
{
"Instances": [
{
"InstanceId": "i-0123456789abcdef0",
"State": {"Name": "running"},
"InstanceType": "t2.micro",
"ImageId": "ami-87654321",
"LaunchTime": "2026-11-12T11:34:56.000Z",
"PrivateDnsName": "ip-172-31-32-101.ec2.internal",
}
]
}
]
}
elif operation_name == "DescribeImages":
if "Owners" in kwarg and kwarg["Owners"] == ["amazon"]:
return {
"Images": [
{
"ImageId": "ami-87654321",
"DeprecationTime": "2022-01-01T00:00:00.000Z",
"Public": True,
}
]
}
return make_api_call(self, operation_name, kwarg)


class Test_ec2_instance_with_outdated_ami:
@mock_aws
def test_ec2_no_instances(self):
from prowler.providers.aws.services.ec2.ec2_service import EC2

aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.ec2.ec2_instance_with_outdated_ami.ec2_instance_with_outdated_ami.ec2_client",
new=EC2(aws_provider),
),
):
from prowler.providers.aws.services.ec2.ec2_instance_with_outdated_ami.ec2_instance_with_outdated_ami import (
ec2_instance_with_outdated_ami,
)

check = ec2_instance_with_outdated_ami()
result = check.execute()

assert len(result) == 0

@mock.patch(
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_private
)
def test_ec2_no_public_images(self):
from prowler.providers.aws.services.ec2.ec2_service import EC2

aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.ec2.ec2_instance_with_outdated_ami.ec2_instance_with_outdated_ami.ec2_client",
new=EC2(aws_provider),
),
):
from prowler.providers.aws.services.ec2.ec2_instance_with_outdated_ami.ec2_instance_with_outdated_ami import (
ec2_instance_with_outdated_ami,
)

check = ec2_instance_with_outdated_ami()
result = check.execute()

assert len(result) == 0

@mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
def test_instance_ami_not_outdated(self):
from prowler.providers.aws.services.ec2.ec2_service import EC2

aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.ec2.ec2_instance_with_outdated_ami.ec2_instance_with_outdated_ami.ec2_client",
new=EC2(aws_provider),
),
):
from prowler.providers.aws.services.ec2.ec2_instance_with_outdated_ami.ec2_instance_with_outdated_ami import (
ec2_instance_with_outdated_ami,
)

check = ec2_instance_with_outdated_ami()
result = check.execute()

assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].resource_id == "i-0123456789abcdef0"
assert (
result[0].status_extended
== "EC2 Instance i-0123456789abcdef0 is not using an outdated AMI."
)

@mock.patch(
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_outdated_ami
)
def test_instance_ami_outdated(self):
from prowler.providers.aws.services.ec2.ec2_service import EC2

aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])

with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
mock.patch(
"prowler.providers.aws.services.ec2.ec2_instance_with_outdated_ami.ec2_instance_with_outdated_ami.ec2_client",
new=EC2(aws_provider),
),
):
from prowler.providers.aws.services.ec2.ec2_instance_with_outdated_ami.ec2_instance_with_outdated_ami import (
ec2_instance_with_outdated_ami,
)

check = ec2_instance_with_outdated_ami()
result = check.execute()

assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].resource_id == "i-0123456789abcdef0"
assert (
result[0].status_extended
== "EC2 Instance i-0123456789abcdef0 is using outdated AMI ami-87654321."
)
1 change: 1 addition & 0 deletions tests/providers/aws/services/ec2/ec2_service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,7 @@ def test_describe_images(self):
ec2.images[0].arn
== f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:image/{ec2.images[0].id}"
)
assert ec2.images[0].deprecation_time == instance.image.deprecation_time
assert not ec2.images[0].public
assert ec2.images[0].region == AWS_REGION_US_EAST_1
assert ec2.images[0].tags == [
Expand Down
Loading