I tried resolving/suppressing GuardDuty sample findings in SecurityHub collectively using AWS CLI (shell script)

I tried resolving/suppressing GuardDuty sample findings in SecurityHub collectively using AWS CLI (shell script)

2025.08.15

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
https://dev.classmethod.jp/articles/create-a-single-sample-findings-in-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.

Findings _ Security Hub CSPM _ ap-northeast-1 - Google

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 or brew 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 or SUPPRESSED). Default is RESOLVED.
  • 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

https://dev.classmethod.jp/articles/create-a-single-sample-findings-in-guardduty/

Share this article

FacebookHatena blogX

Related articles