mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
feat(aws): add new check ec2_instance_with_outdated_ami (#6910)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
This commit is contained in:
committed by
GitHub
parent
837c65ba23
commit
cdb455b2b1
@@ -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
|
||||
|
||||
+30
@@ -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": ""
|
||||
}
|
||||
+52
@@ -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] = []
|
||||
|
||||
|
||||
+221
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user