I tried resolving/suppressing GuardDuty sample findings in SecurityHub collectively using AWS CLI (shell script)
I'm from the Cloud Business Division at the Shin Fukuoka Office.
When you click on GuardDuty's sample detection results, a large number of detection results are generated. When you click, a large number of detection results are generated. (Important...)
As of August 15, 2025, 363 results (Tokyo region) are generated.
If you want to generate just one case, please refer to the following:
How to generate only one sample event in Amazon GuardDuty
There's honestly nothing we can do about what's already been generated, but they also appear in AWS Security Hub CSPM detection results, mixed with actual detection results, making visibility quite poor.
It's difficult to mark GuardDuty sample detection results in AWS Security Hub console one by one (even in batches of 20) as "RESOLVED" or "SUPPRESSED." So, I'll introduce a method using an AWS CLI script to batch process sample detection results with pagination support.
By the way, if you want to filter only sample detection results in the AWS console, you can filter by Sample Finding=True.
You can also handle them this way if you prefer clicking through the interface.
As a bonus, I'll also introduce how to prevent the generation of sample detection results using Service Control Policies (SCPs) at the end.
Note that I used generative AI to create the scripts. While I've thoroughly tested their operation, I can't guarantee they will work in all environments, so please understand.
Prerequisites
- AWS CLI is installed with appropriate permissions (
securityhub:GetFindings
,securityhub:BatchUpdateFindings
). jq
is installed (e.g.,apt install jq
orbrew install jq
). It's already installed on AWS CloudShell.- Security Hub and GuardDuty are enabled
- Since detections are region-specific, please implement this for each region.## How to use the script
Save the script described below to update_guardduty_samples.sh
and grant execution permission. Its operation has been confirmed in AWS CloudShell.
chmod +x update_guardduty_samples.sh
./update_guardduty_samples.sh [RESOLVED|SUPPRESSED] [dry-run]
- Argument 1: Update status (
RESOLVED
orSUPPRESSED
). Default isRESOLVED
. - Argument 2: Specifying
dry-run
will only retrieve targets without implementing updates.
Output example (during dry-run):
$ ./update_guardduty_samples.sh RESOLVED dry-run
[DRY RUN] Would update 100 findings to RESOLVED.
[DRY RUN] Would update 100 findings to RESOLVED.
[DRY RUN] Would update 100 findings to RESOLVED.
[DRY RUN] Would update 63 findings to RESOLVED.
Total updates: 0 (Unprocessed: 0)
Output example (during actual execution):
$ ./update_guardduty_samples.sh RESOLVED
Updated 100 findings (Unprocessed: 0).
Updated 100 findings (Unprocessed: 0).
Updated 100 findings (Unprocessed: 0).
Updated 63 findings (Unprocessed: 0).
Total updates: 363 (Unprocessed: 0)
Pagination support is implemented, so it can process more than 100 items.## Script Full Text
Below is the script.
#!/bin/bash
# Script usage: ./update_guardduty_samples.sh [RESOLVED|SUPPRESSED] [dry-run]
# Example: ./update_guardduty_samples.sh RESOLVED dry-run
# Specifying dry-run will check the number of affected items without updating them.
# Prerequisites: AWS CLI installed with appropriate permissions. jq is required.
STATUS="${1:-RESOLVED}" # Default is RESOLVED
DRY_RUN="$2"
REGION="ap-northeast-1" # Region (please change as needed)
MAX_RESULTS=100 # Page size
SLEEP_INTERVAL=1 # Delay to avoid API rate limits (seconds)
NOTE_TEXT="Sample finding therefore ${STATUS}"
NOTE_UPDATED_BY="update-guardduty-samples-script"
if [ "$STATUS" != "RESOLVED" ] && [ "$STATUS" != "SUPPRESSED" ]; then
echo "Usage: $0 [RESOLVED|SUPPRESSED] [dry-run]"
exit 1
fi
if [ "$DRY_RUN" != "dry-run" ] && [ ! -z "$DRY_RUN" ]; then
echo "Usage: $0 [RESOLVED|SUPPRESSED] [dry-run]"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required but not installed. Install jq (e.g., apt install jq or brew install jq)."
exit 1
fi
# Initialize variables
NEXT_TOKEN=""
TOTAL_UPDATED=0
TOTAL_UNPROCESSED=0
while true; do
# Execute get-findings
if [ -z "$NEXT_TOKEN" ]; then
RESPONSE=$(aws securityhub get-findings \
--filters '{"ProductName":[{"Value":"GuardDuty","Comparison":"EQUALS"}],"Sample":[{"Value":true}],"RecordState":[{"Value":"ACTIVE","Comparison":"EQUALS"}],"WorkflowStatus":[{"Value":"NEW","Comparison":"EQUALS"},{"Value":"NOTIFIED","Comparison":"EQUALS"}]}' \
--max-results $MAX_RESULTS \
--region $REGION \
--output json 2>&1)
else
RESPONSE=$(aws securityhub get-findings \
--filters '{"ProductName":[{"Value":"GuardDuty","Comparison":"EQUALS"}],"Sample":[{"Value":true}],"RecordState":[{"Value":"ACTIVE","Comparison":"EQUALS"}],"WorkflowStatus":[{"Value":"NEW","Comparison":"EQUALS"},{"Value":"NOTIFIED","Comparison":"EQUALS"}]}' \
--max-results $MAX_RESULTS \
--next-token "$NEXT_TOKEN" \
--region $REGION \
--output json 2>&1)
fi
if [[ $RESPONSE == *"An error occurred"* ]]; then
echo "Error in get-findings: $RESPONSE"
exit 1
fi
FINDINGS=$(echo "$RESPONSE" | jq '.Findings')
NUM_FINDINGS=$(echo "$FINDINGS" | jq 'length')
if [ "$NUM_FINDINGS" -eq 0 ]; then
echo "No more findings found."
break
fi
IDENTIFIERS=$(echo "$FINDINGS" | jq '[.[] | {"Id": .Id, "ProductArn": .ProductArn}]')
if [ "$DRY_RUN" = "dry-run" ]; then
echo "[DRY RUN] Would update $NUM_FINDINGS findings to $STATUS."
else
UPDATE_RESPONSE=$(aws securityhub batch-update-findings \
--finding-identifiers "$IDENTIFIERS" \
--workflow '{"Status": "'"$STATUS"'"}' \
--note '{"Text": "'"$NOTE_TEXT"'", "UpdatedBy": "'"$NOTE_UPDATED_BY"'"}' \
--region $REGION \
--output json 2>&1)
if [[ $UPDATE_RESPONSE == *"An error occurred"* ]]; then
echo "Error in batch-update-findings: $UPDATE_RESPONSE"
sleep $SLEEP_INTERVAL
continue
fi
PROCESSED=$(echo "$UPDATE_RESPONSE" | jq '.ProcessedFindings | length')
UNPROCESSED=$(echo "$UPDATE_RESPONSE" | jq '.UnprocessedFindings | length')
TOTAL_UPDATED=$((TOTAL_UPDATED + PROCESSED))
TOTAL_UNPROCESSED=$((TOTAL_UNPROCESSED + UNPROCESSED))
echo "Updated $PROCESSED findings (Unprocessed: $UNPROCESSED)."
fi
NEXT_TOKEN=$(echo "$RESPONSE" | jq -r '.NextToken // empty')
if [ -z "$NEXT_TOKEN" ]; then
break
fi
sleep $SLEEP_INTERVAL
done
echo "Total updates: $TOTAL_UPDATED (Unprocessed: $TOTAL_UNPROCESSED)"
```## Script Points
- **Pagination Support**: `get-findings` uses NextToken to loop through results even when there are more than 100 findings
- **dry-run mode**: Specify `dry-run` to check the number of target items without updating
- **Error Handling**: Logs AWS CLI errors and unprocessed findings. If you hit rate limits, adjust the `SLEEP_INTERVAL`
- **Customizability**: Allows changing `STATUS` (`RESOLVED`/`SUPPRESSED`) and `REGION` as needed.
**Note**:
- When there are thousands of findings, be aware of API rate limits. Increase the `SLEEP_INTERVAL` if necessary.
## Extra: Preventing Sample Finding Generation with SCP
To prevent accidental GuardDuty sample findings creation, you can use Service Control Policies (SCPs) in AWS Organizations to restrict the `guardduty:CreateSampleFindings` action.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyGuardDutySampleFindings",
"Effect": "Deny",
"Action": ["guardduty:CreateSampleFindings"],
"Resource": ["*"]
}
]
}
Summary
You can use this to bulk process (suppress or resolve) Security Hub's GuardDuty sample findings when needed.
Additionally, you can prevent the generation of sample findings using SCP to avoid operational mistakes.
References