AWS GuardDuty is the primary threat detection service for AWS environments — but "enabled" is not the same as "effective." GuardDuty analyzes CloudTrail, VPC Flow Logs, DNS logs, and S3 data events, but attackers using legitimate API calls from trusted IP ranges, staging attacks through already-whitelisted services, or staying within GuardDuty's finding thresholds can operate undetected. This guide covers systematic GuardDuty security testing: validating detection coverage, identifying gaps, testing bypass techniques, and verifying your alert response pipeline works end-to-end.
| Data Source | Findings Enabled | Requires Extra Config |
|---|---|---|
| CloudTrail management events | IAM, EC2, S3 API abuse | No (included by default) |
| CloudTrail S3 data events | S3 object-level access anomalies | Yes — must enable S3 protection |
| VPC Flow Logs | Network anomalies, port scans, C2 | No (analyzed by GuardDuty, not your Flow Logs) |
| DNS logs | DNS-based C2, DGA detection | No (only captures Route 53 resolver queries) |
| EKS audit logs | K8s API abuse, privilege escalation | Yes — must enable EKS protection |
| Lambda network activity | Lambda-based exfiltration | Yes — must enable Lambda protection |
| RDS login activity | Credential brute force, anomalous access | Yes — must enable RDS protection |
| Malware protection (EBS) | Malware on EC2 volumes | Yes — paid add-on |
# Check which GuardDuty protection plans are enabled
aws guardduty get-detector --detector-id $(aws guardduty list-detectors --query 'DetectorIds[0]' --output text) \
--query '{Status:Status,S3Protection:DataSources.S3Logs.Status,DNSLogs:DataSources.DNSLogs.Status,EKS:Features[?Name==`EKS_AUDIT_LOGS`].Status,Lambda:Features[?Name==`LAMBDA_NETWORK_LOGS`].Status,RDS:Features[?Name==`RDS_LOGIN_EVENTS`].Status}' \
--output table
# Check if GuardDuty is enabled in ALL regions (common gap: only enabled in primary region)
for region in us-east-1 us-east-2 us-west-1 us-west-2 eu-west-1 eu-central-1 ap-southeast-1 ap-northeast-1; do
status=$(aws guardduty list-detectors --region $region --query 'DetectorIds[0]' --output text 2>/dev/null)
[[ -z "$status" ]] && echo "[$region] NOT ENABLED" || echo "[$region] Detector: $status"
done
# Check for suppression rules (can blind GuardDuty to attacks)
DETECTOR=$(aws guardduty list-detectors --query 'DetectorIds[0]' --output text)
aws guardduty list-filters --detector-id $DETECTOR | \
python3 -c "import json,sys; [print(f) for f in json.load(sys.stdin)['FilterNames']]"
# Inspect each suppression filter for overly broad criteria
for filter in $(aws guardduty list-filters --detector-id $DETECTOR --query 'FilterNames[]' --output text); do
echo "=== Filter: $filter ==="
aws guardduty get-filter --detector-id $DETECTOR --filter-name $filter \
--query '{Action:Action,Criteria:FindingCriteria}' --output json
done
# GuardDuty provides a built-in sample finding generator
# Use this to validate your alert response pipeline before testing real attacks
# Generate all sample finding types
DETECTOR=$(aws guardduty list-detectors --query 'DetectorIds[0]' --output text)
aws guardduty create-sample-findings --detector-id $DETECTOR \
--finding-types "UnauthorizedAccess:IAMUser/MaliciousIPCaller" \
"Recon:IAMUser/MaliciousIPCaller" \
"PrivilegeEscalation:IAMUser/AdministrativePermissions" \
"CryptoCurrency:EC2/BitcoinTool.B" \
"Trojan:EC2/BlackholeTraffic" \
"Backdoor:EC2/C&CActivity.B"
# Verify sample findings appeared (within ~5 minutes)
aws guardduty list-findings --detector-id $DETECTOR \
--finding-criteria '{"Criterion":{"service.additionalInfo.sample":{"Eq":["true"]}}}' \
--query 'FindingIds' --output text
# Trace the alert path: GuardDuty → EventBridge → SNS → PagerDuty/Slack
# Check EventBridge rule for GuardDuty
aws events list-rules --query 'Rules[?EventPattern!=`null`]' | \
python3 -c "
import json,sys
for r in json.load(sys.stdin):
try:
pattern = json.loads(r.get('EventPattern','{}'))
if 'aws.guardduty' in str(pattern):
print(f\"Rule: {r['Name']} State: {r['State']}\")
except: pass
"
# Verify SNS notification was received within SLA
aws cloudwatch get-metric-statistics \
--namespace AWS/Events \
--metric-name FailedInvocations \
--dimensions Name=RuleName,Value=guardduty-alert-rule \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-1H +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 3600 --statistics Sum
# GuardDuty has a "Trusted IP List" feature — IPs in this list generate no findings
# If an attacker compromises an IP on this list, all their actions are invisible to GuardDuty
# Check trusted IP lists configured
aws guardduty list-ip-sets --detector-id $DETECTOR
aws guardduty get-ip-set --detector-id $DETECTOR --ip-set-id
# Download and inspect the IP list for any overly broad ranges (e.g., /8 subnets)
# Check threat intel sets (custom blocklists)
aws guardduty list-threat-intel-sets --detector-id $DETECTOR
# Many GuardDuty findings have rate thresholds or require anomalous behavior patterns
# Slow and steady reconnaissance often avoids detection
# IAM reconnaissance without triggering "Recon:IAMUser/UserPermissions"
# The finding triggers on rapid enumeration — slow it down
python3 << 'EOF'
import boto3, time
iam = boto3.client('iam')
# Enumerate IAM policies slowly (avoid burst-based detection)
actions = [
lambda: iam.get_account_authorization_details(Filter=['User']),
lambda: iam.list_roles(MaxItems=100),
lambda: iam.list_policies(Scope='Local', MaxItems=100),
]
for action in actions:
try:
result = action()
print(f"OK: {list(result.keys())[:3]}")
except Exception as e:
print(f"Error: {e}")
time.sleep(30) # 30-second delay between each enumeration call
EOF
# S3 enumeration that avoids GuardDuty S3 findings
# GuardDuty S3: "Discovery:S3/MaliciousIPCaller" triggers on known-bad IPs
# From a clean IP: slow enumeration of S3 metadata is not detected
aws s3api list-buckets # Baseline — always works
sleep 10
aws s3api get-bucket-acl --bucket target-bucket # Check ACL
sleep 10
aws s3api get-bucket-policy --bucket target-bucket 2>/dev/null # Check policy
# GuardDuty uses CloudTrail as a data source
# If an attacker has sufficient IAM permissions, they can interfere with CloudTrail
# This generates GuardDuty finding: "Stealth:IAMUser/CloudTrailLoggingDisabled"
# But if that finding's alert chain is broken, they could operate in the dark
# Method: Delete CloudTrail trail (generates a finding, but if response pipeline is slow...)
aws cloudtrail delete-trail --name my-trail
# GuardDuty generates: Stealth:IAMUser/CloudTrailLoggingDisabled (within ~15min)
# Method: Stop logging without deleting
aws cloudtrail stop-logging --name my-trail
# Method: Create EventBridge rule to drop GuardDuty events before they reach SNS
# (Requires cloudformation:CreateStack or events:PutRule)
# Method: Modify GuardDuty suppression rules to suppress ALL findings
aws guardduty create-filter --detector-id $DETECTOR \
--name "temp-suppress-all" \
--action SUPPRESS \
--rank 1 \
--finding-criteria '{"Criterion":{"severity":{"Gte":0}}}'
# This silences ALL GuardDuty findings — requires guardduty:CreateFilter permission
# Audit existing suppression rules for bypass opportunities
# A suppression rule that's too broad silences legitimate attack alerts
# Example dangerous suppression: "suppress all findings from this IAM role"
# An attacker who can assume that role operates without detection
aws guardduty list-filters --detector-id $DETECTOR --output text | \
while read filter; do
echo "=== $filter ==="
aws guardduty get-filter --detector-id $DETECTOR --filter-name $filter \
--query 'FindingCriteria.Criterion' --output json
done
# Test if a suppressed IAM role generates findings
# 1. Create a test IAM role
# 2. Add it to a suppression filter
# 3. Perform a known-detectable action (e.g., GetPasswordData) from that role
# 4. Verify no finding appears in GuardDuty console
# Safe test: use sample findings to check if suppression blocks them
aws guardduty create-sample-findings --detector-id $DETECTOR \
--finding-types "UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B"
# Check if the sample finding is suppressed (not in active findings)
aws guardduty list-findings --detector-id $DETECTOR \
--finding-criteria '{"Criterion":{"service.additionalInfo.sample":{"Eq":["true"]}}}' \
--query 'length(FindingIds)'
# GuardDuty generates findings for IAM enumeration from known-malicious IPs
# From a clean/trusted IP, many IAM enumeration techniques go undetected
# Determine current effective permissions (no GuardDuty finding for this)
aws sts get-caller-identity
aws iam get-user 2>/dev/null || echo "Likely using role/instance profile"
# List attached policies (usually not detected)
aws iam list-attached-user-policies --user-name $(aws iam get-user --query 'User.UserName' --output text 2>/dev/null)
aws iam list-groups-for-user --user-name $(aws iam get-user --query 'User.UserName' --output text 2>/dev/null)
# Enumerate services accessible (no GuardDuty finding for simple API calls from clean IP)
for service in ec2 s3 iam lambda rds secretsmanager ssm kms sts; do
echo -n "$service: "
aws $service describe-instances 2>/dev/null | head -1 || \
aws $service list-buckets 2>/dev/null | head -1 || \
aws $service get-user 2>/dev/null | head -1 || \
echo "access denied or not applicable"
done
# Check what GuardDuty WOULD find for specific actions
# Reference GuardDuty finding types to understand the detection model:
# Recon:IAMUser/UserPermissions — rapid GetUser, ListUsers, GetLoginProfile calls
# Recon:IAMUser/NetworkPermissions — rapid EC2 DescribeInstances, DescribeSecurityGroups
# Impact:IAMUser/AnomalousBehavior — unusual API for this principal
# PrivilegeEscalation:IAMUser/AdministrativePermissions — attaching AdministratorAccess
# Full end-to-end test: GuardDuty → EventBridge → SNS → Lambda/PagerDuty
# 1. Check EventBridge rule configuration
aws events list-rules --event-bus-name default | \
python3 -c "
import json,sys
for r in json.load(sys.stdin).get('Rules',[]):
pattern = json.loads(r.get('EventPattern','{}'))
if pattern.get('source') == ['aws.guardduty']:
print(f'Rule: {r[\"Name\"]} State: {r[\"State\"]}')
targets = r.get('Targets',[])
print(f' Targets: {[t[\"Arn\"] for t in targets]}')
"
# 2. Measure time from finding creation to alert receipt (MTTD)
# Generate a sample finding and timestamp it
START=$(date -u +%s)
aws guardduty create-sample-findings --detector-id $DETECTOR \
--finding-types "UnauthorizedAccess:IAMUser/MaliciousIPCaller"
echo "Finding created at: $(date -u)"
# Watch for SNS/PagerDuty/Slack notification
# Time from $START to notification = MTTD
# 3. Verify HIGH and CRITICAL severity findings have different alert paths than LOW
# Check if severity filters are configured on EventBridge rules
aws events list-rules | python3 -c "
import json,sys
for r in json.load(sys.stdin).get('Rules',[]):
p = json.loads(r.get('EventPattern','{}'))
if 'guardduty' in str(p):
severity_filter = p.get('detail',{}).get('severity',[])
print(f'Rule: {r[\"Name\"]} Severity filter: {severity_filter or \"ALL\"}')
"
# 4. Test cross-region GuardDuty finding aggregation
# If using GuardDuty in multiple regions, verify delegated admin and aggregation
aws guardduty list-organization-admin-accounts 2>/dev/null || echo "Not an org admin"
# 1. Enable GuardDuty in ALL AWS regions
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
existing=$(aws guardduty list-detectors --region $region --query 'DetectorIds[0]' --output text 2>/dev/null)
if [[ -z "$existing" || "$existing" == "None" ]]; then
aws guardduty create-detector --enable --region $region
echo "Enabled GuardDuty in $region"
fi
done
# 2. Enable all protection plans
aws guardduty update-detector --detector-id $DETECTOR \
--features '[
{"Name":"S3_DATA_EVENTS","Status":"ENABLED"},
{"Name":"EKS_AUDIT_LOGS","Status":"ENABLED"},
{"Name":"LAMBDA_NETWORK_LOGS","Status":"ENABLED"},
{"Name":"RDS_LOGIN_EVENTS","Status":"ENABLED"},
{"Name":"EBS_MALWARE_PROTECTION","Status":"ENABLED"}
]'
# 3. Configure EventBridge rule for ALL finding severities
aws events put-rule \
--name "guardduty-all-findings" \
--event-pattern '{"source":["aws.guardduty"],"detail-type":["GuardDuty Finding"]}' \
--state ENABLED
# 4. Set up severity-based routing
# HIGH/CRITICAL → PagerDuty (immediate page)
# MEDIUM → Slack + ticket
# LOW → daily digest
# 5. Review and tighten suppression rules — never suppress by IAM role alone
# Minimum: suppress only specific finding type + specific resource + specific IP
# 6. Enable GuardDuty organization-wide via AWS Organizations delegated admin
aws guardduty enable-organization-admin-account --admin-account-id SECURITY_ACCOUNT_ID
# 7. Set finding export to S3 for long-term retention and threat hunting
aws guardduty update-detector --detector-id $DETECTOR \
--finding-publishing-frequency FIFTEEN_MINUTES
| Security Test | Method | Gap Indicator |
|---|---|---|
| Regional coverage | list-detectors in all regions | Missing detector = blind region |
| S3 protection enabled | get-detector Features check | S3 data events not analyzed |
| Alert pipeline end-to-end | create-sample-findings + wait | No notification = broken pipeline |
| Suppression rule audit | list-filters + get-filter | Broad suppression = detection gap |
| Trusted IP list audit | list-ip-sets + download | Overly broad IP range = bypass |
| MTTD measurement | Timestamp finding vs alert | >15min MTTD = insufficient |
| CloudTrail tampering detection | Attempt CloudTrail stop, verify finding | No finding = tamper detection gap |
Ironimo validates your GuardDuty deployment by testing detection coverage across attack categories, verifying the alert response pipeline end-to-end, and identifying suppression rules and configuration gaps that leave your AWS environment exposed.
Start free scan