feat(aws): add new check ec2_instance_with_outdated_ami (#6910)

Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
Hugo Pereira Brito
2025-09-30 19:54:36 +02:00
committed by GitHub
parent 837c65ba23
commit cdb455b2b1
8 changed files with 358 additions and 32 deletions
+1
View File
@@ -7,6 +7,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Added
- Support for AdditionalURLs in outputs [(#8651)](https://github.com/prowler-cloud/prowler/pull/8651)
- Support for markdown metadata fields in Dashboard [(#8667)](https://github.com/prowler-cloud/prowler/pull/8667)
- `ec2_instance_with_outdated_ami` check for AWS provider [(#6910)](https://github.com/prowler-cloud/prowler/pull/6910)
- LLM provider using `promptfoo` [(#8555)](https://github.com/prowler-cloud/prowler/pull/8555)
- Documentation for renaming checks [(#8717)](https://github.com/prowler-cloud/prowler/pull/8717)
- Add explicit "name" field for each compliance framework and include "FRAMEWORK" and "NAME" in CSV output [(#7920)](https://github.com/prowler-cloud/prowler/pull/7920)
@@ -6,15 +6,16 @@ class ec2_ami_public(Check):
def execute(self):
findings = []
for image in ec2_client.images:
report = Check_Report_AWS(metadata=self.metadata(), resource=image)
report.status = "PASS"
report.status_extended = (
f"EC2 AMI {image.name if image.name else image.id} is not public."
)
if image.public:
report.status = "FAIL"
report.status_extended = f"EC2 AMI {image.name if image.name else image.id} is currently public."
if image.owner == "self":
report = Check_Report_AWS(metadata=self.metadata(), resource=image)
report.status = "PASS"
report.status_extended = (
f"EC2 AMI {image.name if image.name else image.id} is not public."
)
if image.public:
report.status = "FAIL"
report.status_extended = f"EC2 AMI {image.name if image.name else image.id} is currently public."
findings.append(report)
findings.append(report)
return findings
return findings
@@ -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://repost.aws/knowledge-center/ec2-find-deprecated-ami",
"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://repost.aws/knowledge-center/ec2-find-deprecated-ami"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -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.owner == "amazon":
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
@@ -349,20 +349,30 @@ class EC2(AWSService):
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"),
owner=owner,
)
)
except Exception as error:
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
@@ -746,6 +756,8 @@ class Image(BaseModel):
arn: str
name: str
public: bool
deprecation_time: Optional[str]
owner: str
region: str
tags: Optional[list] = []
@@ -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."
)
@@ -610,16 +610,21 @@ class Test_EC2_Service:
)
ec2 = EC2(aws_provider)
assert len(ec2.images) == 1
assert ec2.images[0].id == image_id
assert re.match(r"ami-[0-9a-z]{8}", ec2.images[0].id)
# Filter for user-created images (owner="self")
user_images = [img for img in ec2.images if img.owner == "self"]
assert len(user_images) == 1
user_image = user_images[0]
assert user_image.id == image_id
assert re.match(r"ami-[0-9a-z]{8}", user_image.id)
assert (
ec2.images[0].arn
== f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:image/{ec2.images[0].id}"
user_image.arn
== f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:image/{user_image.id}"
)
assert not ec2.images[0].public
assert ec2.images[0].region == AWS_REGION_US_EAST_1
assert ec2.images[0].tags == [
assert user_image.deprecation_time == instance.image.deprecation_time
assert not user_image.public
assert user_image.region == AWS_REGION_US_EAST_1
assert user_image.tags == [
{
"Key": "Base_AMI_Name",
"Value": "Deep Learning Base AMI (Amazon Linux 2) Version 31.0",
@@ -627,6 +632,10 @@ class Test_EC2_Service:
{"Key": "OS_Version", "Value": "AWS Linux 2"},
]
# Verify that Amazon images are also present
amazon_images = [img for img in ec2.images if img.owner == "amazon"]
assert len(amazon_images) > 0 # Should have Amazon AMIs
# Test EC2 Describe Volumes
@mock_aws
def test_describe_volumes(self):