Back to Checks
aws

EC2 instance has not been stopped longer than the configured maximum days

ec2_instance_stopped_older_than_specific_days
Service: ec2Severity: low
OfficialBuilt-inVerified
Resource Type:AwsEc2Instance

EC2 instances in the stopped state are evaluated for how long they have remained stopped. Instances stopped beyond the configurable limit (max_ec2_instance_stopped_days, default 30) are flagged. Running, pending, and other non-stopped instances pass.

Risk

Long-stopped instances remain unmonitored and unpatched while still retaining EBS volumes, network configuration, IAM instance profiles, and SSH keys. This expands attack surface and incurs unnecessary storage cost. Stale AMIs and credentials attached to abandoned instances increase the chance of compromise if the instance is later started.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks ec2_instance_stopped_older_than_specific_days

Recommendation

Establish a lifecycle policy for stopped instances:

  • Tag instances with owner and expiry
  • Terminate instances no longer needed to reclaim EBS cost
  • If retained, start regularly for patching or rebuild from a hardened AMI
  • Prefer ephemeral, autoscaled workloads over long-lived stopped hosts

Adjust max_ec2_instance_stopped_days to match policy.

Remediation

CLI

aws ec2 terminate-instances --instance-ids <example_resource_id>

Other

1Sign in to the AWS Management Console and open EC2
2Go to Instances and select the long-stopped instance
3Review attached EBS volumes, tags, and ownership
4Choose Instance state > Terminate instance (or Start instance if still needed, then patch/rebuild)
5Confirm and verify the instance is terminated or returned to an actively managed lifecycle

Check Code

import re
from datetime import datetime, timezone
from typing import Optional

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

# AWS StateTransitionReason for stopped instances, e.g.:
# "User initiated (2016-09-14 15:07:39 GMT)"
_STATE_TRANSITION_TIME_REGEX = re.compile(
    r"\((\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) GMT\)"
)


def _parse_state_transition_time(
    state_transition_reason: Optional[str],
) -> Optional[datetime]:
    """Extract the stop timestamp from EC2 StateTransitionReason."""
    if not state_transition_reason:
        return None
    match = _STATE_TRANSITION_TIME_REGEX.search(state_transition_reason)
    if not match:
        return None
    try:
        return datetime.strptime(match.group(1), "%Y-%m-%d %H:%M:%S").replace(
            tzinfo=timezone.utc
        )
    except ValueError:
        return None


class ec2_instance_stopped_older_than_specific_days(Check):
    """Ensure EC2 instances are not stopped longer than a configured number of days.

    Evaluates each instance's stop duration using StateTransitionReason.
    - PASS: Instance is not stopped, or has been stopped for at most the threshold.
    - FAIL: Instance has been stopped longer than max_ec2_instance_stopped_days
      (default 30).
    """

    def execute(self) -> list[Check_Report_AWS]:
        """Execute the check logic.

        Returns:
            A list of reports containing the result of the check.
        """
        findings = []

        # max_ec2_instance_stopped_days, default: 30 days
        max_ec2_instance_stopped_days = ec2_client.audit_config.get(
            "max_ec2_instance_stopped_days", 30
        )
        for instance in ec2_client.instances:
            report = Check_Report_AWS(metadata=self.metadata(), resource=instance)
            report.resource_id = instance.id
            report.resource_arn = instance.arn
            report.resource_tags = instance.tags
            report.status = "PASS"
            report.status_extended = f"EC2 Instance {instance.id} is not stopped."
            if instance.state == "stopped":
                stop_time = _parse_state_transition_time(
                    instance.state_transition_reason
                )
                if not stop_time:
                    report.status_extended = (
                        f"EC2 Instance {instance.id} is stopped but stop time "
                        f"could not be determined."
                    )
                else:
                    days_stopped = (datetime.now(timezone.utc) - stop_time).days
                    report.status_extended = (
                        f"EC2 Instance {instance.id} has not been stopped longer "
                        f"than {max_ec2_instance_stopped_days} days "
                        f"({days_stopped} days)."
                    )
                    if days_stopped > max_ec2_instance_stopped_days:
                        report.status = "FAIL"
                        report.status_extended = (
                            f"EC2 Instance {instance.id} has been stopped longer "
                            f"than {max_ec2_instance_stopped_days} days "
                            f"({days_stopped} days)."
                        )

            findings.append(report)

        return findings