Automated EC2 Malware Scanning with AWS GuardDuty and SAM
This guide details how to automate on-demand malware scans for your Amazon EC2 instances using AWS GuardDuty, orchestrated via a simple AWS Serverless Application Model (SAM) template and a Python Lambda function. By following these steps, you can establish a robust, scheduled security check for your running instances.
Essential Prerequisites
Before deploying the automation, ensure the following conditions are met:
- Encrypted EC2 Volumes: All EC2 instances targeted for scanning must have their EBS volumes encrypted with an AWS Key Management Service (KMS) Customer Managed Key (CMK). If your existing EBS volumes are not encrypted or you need to change their encryption key, consult AWS documentation on modifying EBS encryption settings.
- IAM Permissions: You will require appropriate AWS Identity and Access Management (IAM) permissions to deploy AWS SAM applications within your account.
Step-by-Step Implementation
The automation relies on an AWS Lambda function, defined and deployed using an AWS SAM template, which will trigger GuardDuty scans on a predefined schedule.
1. Defining the Lambda Function with AWS SAM
Begin by creating a new SAM template file (e.g., template.yaml
). This file will define your Lambda function, including its runtime, execution schedule, and necessary permissions.
# Simplified representation of the SAM template structure
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
Automates GuardDuty on-demand malware scans for EC2 instances.
Resources:
EC2MalwareScan:
Type: AWS::Serverless::Function
Properties:
FunctionName: ec2-malware-scan-weekly
Description: Initiates GuardDuty on-demand malware scans for running EC2 instances
PackageType: Zip
Runtime: python3.13
Handler: ec2_malware_scan_guardduty.ec2_malware_scan
CodeUri: lambdas/infrastructure/ec2_malware_scan/
Timeout: 60
MemorySize: 256
Tracing: Active
LoggingConfig:
LogFormat: JSON
Architectures:
- x86_64
Events:
WeeklySchedule:
Type: ScheduleV2
Properties:
ScheduleExpression: 'cron(0 6 ? * MON *)' # Runs every Monday at 6 AM UTC
Name: WeeklyEC2MalwareScan
Description: Weekly EC2 malware scan
State: ENABLED
RetryPolicy: # Optional retry configuration
MaximumEventAgeInSeconds: 3600
MaximumRetryAttempts: 2
Policies: # IAM permissions for the Lambda function
- Statement:
- Sid: Ec2Describe
Effect: Allow
Action: ec2:DescribeInstances
Resource: "*"
- Sid: GuardDutyScanOnly
Effect: Allow
Action:
- guardduty:ListDetectors
- guardduty:GetDetector
- guardduty:StartMalwareScan
Resource: "*"
- Sid: IAMPermissions
Effect: Allow
Action:
- iam:GetRole
- iam:PassRole
Resource: "arn:aws:iam::*:role/aws-service-role/malware-protection.guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDutyMalwareProtection"
- Sid: StsCaller
Effect: Allow
Action: sts:GetCallerIdentity
Resource: "*"
Environment:
Variables:
EXCLUDED_INSTANCES: "" # Comma-separated list of Instance IDs to exclude
Key components of this SAM template:
- Lambda Function (
EC2MalwareScan
): Configured with Python 3.13 runtime, a 60-second timeout, and 256MB memory. - Scheduled Execution: The
WeeklySchedule
EventBridge rule triggers the Lambda function every Monday at 6 AM UTC. This schedule can be customized to fit your operational needs. - IAM Permissions: The function is granted specific permissions:
ec2:DescribeInstances
: To discover running EC2 instances.guardduty:ListDetectors
,guardduty:GetDetector
,guardduty:StartMalwareScan
: To interact with GuardDuty for detector management and scan initiation.iam:GetRole
,iam:PassRole
: To assume the GuardDuty service-linked role required for malware protection.sts:GetCallerIdentity
: For retrieving AWS account identity, useful for logging and context.
- Environment Variables: The
EXCLUDED_INSTANCES
variable allows you to specify a comma-separated list of EC2 instance IDs that should be skipped during the scan.
2. Developing the Python Scan Logic
Next, create the Python script that will be executed by your Lambda function. This script, typically named ec2_malware_scan.py
, should reside in the path specified by CodeUri
in your SAM template (e.g., lambdas/infrastructure/ec2_malware_scan/
).
The Python code utilizes the boto3
AWS SDK to interact with EC2, GuardDuty, and STS services.
# Outline of the Python script (ec2_malware_scan.py)
import boto3
import json
import logging
import os
# Initialize AWS clients
ec2 = boto3.client('ec2')
guardduty = boto3.client('guardduty')
sts = boto3.client('sts')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def _get_detector_id() -> str:
"""Retrieves the GuardDuty detector ID for the current region."""
# ... logic to list and return the first detector ID ...
def _malware_protection_enabled() -> bool:
"""Checks if GuardDuty Malware Protection for EBS volumes is enabled."""
# ... logic to get detector details and check status ...
def _running_instances() -> list:
"""Fetches a list of all running EC2 instances with a 'Name' tag."""
# ... logic to paginate describe_instances calls and filter by 'running' state and 'tag:Name' ...
def _start_scan(instance_arn: str) -> dict:
"""Initiates an on-demand GuardDuty malware scan for a given instance ARN."""
# ... logic to call guardduty.start_malware_scan ...
def ec2_malware_scan(event, context):
"""
Lambda handler function to orchestrate the weekly EC2 malware scans.
"""
logger.info("Initiating Weekly GuardDuty EC2 malware scan.")
# 1. Verify GuardDuty Malware Protection is enabled
if not _malware_protection_enabled():
return {
'statusCode': 400,
'body': json.dumps({'error': 'GuardDuty Malware Protection is not enabled'})
}
# 2. Get all running EC2 instances
instances = _running_instances()
# 3. Filter out excluded instances based on the EXCLUDED_INSTANCES environment variable
excluded_ids = {x.strip() for x in os.environ.get('EXCLUDED_INSTANCES', '').split(',') if x.strip()}
instances_to_scan = [i for i in instances if i['InstanceId'] not in excluded_ids]
logger.info(f"Excluded {len(instances) - len(instances_to_scan)} instance(s); starting scans for {len(instances_to_scan)}.")
# 4. Trigger scans for the identified instances
scan_results = [_start_scan(instance['Arn']) for instance in instances_to_scan]
return {
'statusCode': 200,
'body': json.dumps({
'message': 'On-demand malware scans initiated',
'instances_scanned': len(scan_results),
'instances_excluded': len(instances) - len(instances_to_scan),
'results': scan_results
})
}
Key functions within the Python script:
_get_detector_id()
: Locates the active GuardDuty detector in the current region._malware_protection_enabled()
: Confirms that GuardDuty’s Malware Protection feature for EBS volumes is active._running_instances()
: Gathers a list of all currently running EC2 instances, specifically those with aName
tag._start_scan()
: Invokes the GuardDuty API to initiate a malware scan for a given EC2 instance ARN.ec2_malware_scan(event, context)
: The main Lambda handler function that:- Verifies GuardDuty Malware Protection is enabled.
- Retrieves all running EC2 instances.
- Filters out any instances specified in the
EXCLUDED_INSTANCES
environment variable. - Iterates through the remaining instances and triggers an on-demand malware scan for each.
3. Deployment and Monitoring
After defining your SAM template and Python code, deploy the application using the AWS SAM CLI. Once deployed, the Lambda function will execute according to its schedule (e.g., weekly on Monday mornings), automatically initiating GuardDuty malware scans for your running EC2 instances.
You can monitor the status and results of these on-demand scans directly within the AWS GuardDuty console, under the “Scans” section. This will provide visibility into any potential malware findings and the overall security posture of your EC2 fleet.