Guide · AWS GuardDuty · Findings API

GuardDuty Findings API for MCP Servers — Querying, Filtering, Archiving

GuardDuty deduplicates findings: if the same threat type targets the same resource from the same source, GuardDuty updates the existing finding (incrementing service.count and updating service.eventLastSeen) rather than creating a new finding. This means a finding that appears once in ListFindings may represent hundreds of individual events — the count field is the true incident volume. For MCP server security automation, this deduplication has a critical implication for EventBridge-triggered remediation: the same finding ID fires an EventBridge event on every update cycle, not just on first detection. Your remediation Lambda must be idempotent — checking whether the isolation action has already been applied before re-applying it — because a brute-force attack that runs for hours will re-trigger the finding event every 15 minutes.

TL;DR

Use ListFindings with finding-criteria to filter by severity, type, and resource. Fetch details via GetFindings (max 50 per call). GuardDuty deduplicates: same type+resource+source = update existing finding, not new finding. Archive findings when actioned (not when reviewing). Use CreateFilter with ARCHIVE action for systematic suppression — filters run at generation time, preventing findings from ever appearing active. See the main GuardDuty setup guide for detector configuration and the remediation guide for automated response patterns.

ListFindings with filter criteria

ListFindings returns finding IDs (not finding details) matching the specified criteria. You must call GetFindings with the returned IDs to get the actual finding content. The maximum page size is 50 IDs per ListFindings call and 50 findings per GetFindings call.

# List all unarchived High-severity findings, newest first
aws guardduty list-findings \
  --detector-id abc1234567890abcdef1234567890 \
  --finding-criteria '{
    "Criterion": {
      "severity": {"Gte": 7},
      "service.archived": {"Eq": ["false"]}
    }
  }' \
  --sort-criteria '{"AttributeName": "updatedAt", "OrderBy": "DESC"}' \
  --max-results 50

# List findings of a specific type (EC2 credential exfiltration)
aws guardduty list-findings \
  --detector-id abc1234567890abcdef1234567890 \
  --finding-criteria '{
    "Criterion": {
      "type": {
        "Equals": [
          "InstanceCredentialExfiltration:EC2/NoInstanceProfile",
          "InstanceCredentialExfiltration:EC2/ScheduledEvent"
        ]
      }
    }
  }'

# List findings affecting a specific ECS cluster (by tag)
aws guardduty list-findings \
  --detector-id abc1234567890abcdef1234567890 \
  --finding-criteria '{
    "Criterion": {
      "resource.instanceDetails.tags.key": {"Equals": ["aws:ecs:clusterName"]},
      "resource.instanceDetails.tags.value": {"Equals": ["mcp-production-cluster"]}
    }
  }'

# Paginate through all findings (NextToken pattern)
TOKEN=""
while true; do
  RESULT=$(aws guardduty list-findings \
    --detector-id abc1234567890abcdef1234567890 \
    --max-results 50 \
    ${TOKEN:+--next-token "$TOKEN"} \
    --output json)
  echo "$RESULT" | jq -r '.FindingIds[]'
  TOKEN=$(echo "$RESULT" | jq -r '.NextToken // empty')
  [ -z "$TOKEN" ] && break
done
Filter criterion keyValuesUse case
severityGte/Lte/Gt/Lt/EqFilter by numeric severity (7 = High, 4 = Medium)
typeEquals/Prefix/NotEqualsFilter by finding type string or prefix (e.g., "CryptoCurrency:")
service.archivedEq: ["false"] or ["true"]Show only active (false) or archived (true) findings
updatedAtGte/Lte (epoch ms)Findings updated in a time range
accountIdEqualsIn admin account: filter by member account
regionEqualsIn admin account: filter by member account region
resource.instanceDetails.instanceIdEqualsAll findings for a specific EC2/ECS instance
resource.lambdaDetails.functionNameEqualsAll findings for a specific Lambda function

GetFindings response structure

The GetFindings response includes the full finding JSON for each requested ID. The key fields for MCP server incident response:

# Fetch details for specific finding IDs (max 50 per call)
aws guardduty get-findings \
  --detector-id abc1234567890abcdef1234567890 \
  --finding-ids "abc123" "def456" \
  --query 'Findings[*].{
    Id:Id,
    Type:Type,
    Severity:Severity,
    Count:Service.Count,
    FirstSeen:Service.EventFirstSeen,
    LastSeen:Service.EventLastSeen,
    Resource:Resource
  }'

# Full finding structure (key fields):
# {
#   "Id": "abc123def456...",           ← deterministic hash: same threat = same ID
#   "Type": "UnauthorizedAccess:EC2/SSHBruteForce",
#   "Severity": 5.0,                   ← float 0.1-8.9
#   "CreatedAt": "2026-10-08T10:00:00Z",
#   "UpdatedAt": "2026-10-08T14:00:00Z", ← updated when count increments
#   "Title": "EC2 instance is being probed on port 22.",
#   "Description": "EC2 instance i-1234567890abcdef0 is being probed...",
#
#   "Service": {
#     "Count": 847,                    ← 847 individual SSH attempts, one finding
#     "EventFirstSeen": "2026-10-07T08:00:00Z",
#     "EventLastSeen": "2026-10-08T14:00:00Z",
#     "Archived": false,
#     "UserFeedback": null,            ← "USEFUL" or "NOT_USEFUL" after UpdateFindingsFeedback
#     "Action": {                      ← what threat actor did
#       "ActionType": "PORT_PROBE",
#       "PortProbeAction": {
#         "PortProbeDetails": [{
#           "LocalPortDetails": {"Port": 22, "PortName": "SSH"},
#           "RemoteIpDetails": {
#             "IpAddressV4": "198.51.100.5",
#             "Country": {"CountryName": "China"},
#             "Organization": {"Asn": "4134", "AsnOrg": "CHINANET-BACKBONE"}
#           }
#         }]
#       }
#     }
#   },
#
#   "Resource": {
#     "ResourceType": "Instance",
#     "InstanceDetails": {
#       "InstanceId": "i-1234567890abcdef0",
#       "InstanceType": "t3.medium",
#       "InstanceState": "running",
#       "Tags": [{"Key": "Service", "Value": "mcp-monitoring"}],
#       "NetworkInterfaces": [{
#         "PublicIp": "54.1.2.3",
#         "SubnetId": "subnet-abc123",
#         "VpcId": "vpc-def456"
#       }]
#     }
#   }
# }

The Service.Action field structure varies by finding type: NetworkConnectionAction for connection-based findings, PortProbeAction for port scans, AwsApiCallAction for API-based threats (unauthorized API calls, unusual CloudTrail activity), and DnsRequestAction for DNS-based findings (C2 communication, crypto mining). Always check ActionType first to know which subfield to parse.

Deduplication behavior and implications for automation

GuardDuty generates one finding per unique combination of: finding type + affected resource + source details. Subsequent occurrences of the same threat increment the existing finding's service.count and update service.eventLastSeen rather than creating new findings. The finding ID is deterministic — you can predict it will be the same for repeated events from the same source to the same target.

# Check if a finding is new vs. updated
aws guardduty get-findings \
  --detector-id abc1234567890abcdef1234567890 \
  --finding-ids "abc123" \
  --query 'Findings[0].{
    Created:CreatedAt,
    Updated:UpdatedAt,
    Count:Service.Count,
    IsNew:Service.IsNew
  }'

# Service.IsNew = true → first time this finding appeared in current publishing cycle
# Service.IsNew = false → this is an update to an existing finding
# Count > 1 and CreatedAt != UpdatedAt → repeated threat, not a first occurrence

# For EventBridge-triggered automation: check DynamoDB to see if you already responded
# Using finding ID as the deduplication key in DynamoDB

# Node.js Lambda example (idempotent remediation check):
# const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
# const dynamo = new DynamoDBClient({});
#
# async function handleFinding(findingId, findingType) {
#   // Conditional put: only insert if findingId doesn't exist
#   // Returns ConditionalCheckFailedException if already handled
#   try {
#     await dynamo.send(new PutItemCommand({
#       TableName: 'guardduty-responses',
#       Item: {
#         findingId: { S: findingId },
#         respondedAt: { S: new Date().toISOString() },
#         type: { S: findingType }
#       },
#       ConditionExpression: 'attribute_not_exists(findingId)'
#     }));
#     return true; // First time responding to this finding
#   } catch (e) {
#     if (e.name === 'ConditionalCheckFailedException') return false; // Already handled
#     throw e;
#   }
# }

The deduplication window is 90 days — after 90 days, GuardDuty expires the finding and a subsequent occurrence creates a new finding with a new ID. For ongoing threats (like an EC2 instance under sustained brute-force), you may need to check service.eventLastSeen to determine if the threat is still active, not just whether the finding exists.

Archiving and feedback

Archiving a finding removes it from the active finding list but retains it in the detector for 90 days. Archived findings still appear in ListFindings when you include service.archived: Eq ["true"] in the filter criteria. Use archiving when you have confirmed the finding is either a false positive or has been fully remediated.

# Archive specific findings (mark as reviewed/resolved)
aws guardduty archive-findings \
  --detector-id abc1234567890abcdef1234567890 \
  --finding-ids "finding-id-1" "finding-id-2" "finding-id-3"

# Unarchive if you need to re-open a finding
aws guardduty unarchive-findings \
  --detector-id abc1234567890abcdef1234567890 \
  --finding-ids "finding-id-1"

# Provide feedback to improve GuardDuty's ML model
# USEFUL = this was a real threat, correct detection
# NOT_USEFUL = this was a false positive
aws guardduty update-findings-feedback \
  --detector-id abc1234567890abcdef1234567890 \
  --finding-ids "finding-id-1" \
  --feedback USEFUL \
  --comments "Confirmed credential exfiltration from compromised MCP worker"

aws guardduty update-findings-feedback \
  --detector-id abc1234567890abcdef1234567890 \
  --finding-ids "finding-id-2" \
  --feedback NOT_USEFUL \
  --comments "Expected port scan from internal monitoring tool"

# Bulk archive all Low findings older than 30 days (cleanup script pattern)
# 1. List findings with severity < 4 updated before 30 days ago
# 2. Archive in batches of 50 (API limit per call)
CUTOFF=$(date -u -d '30 days ago' +%s)000  # epoch milliseconds
aws guardduty list-findings \
  --detector-id abc1234567890abcdef1234567890 \
  --finding-criteria "{
    \"Criterion\": {
      \"severity\": {\"Lt\": 4},
      \"updatedAt\": {\"Lte\": $CUTOFF},
      \"service.archived\": {\"Eq\": [\"false\"]}
    }
  }" \
  --query 'FindingIds' --output json | \
  jq -r '.[]' | \
  xargs -n 50 aws guardduty archive-findings \
    --detector-id abc1234567890abcdef1234567890 \
    --finding-ids

Common finding patterns for MCP server infrastructure

Finding typeSeverityWhat it means for MCPImmediate action
InstanceCredentialExfiltration:EC2/NoInstanceProfileHigh (8.0)EC2 instance role credentials used from outside AWS — credentials leaked from MCP server code, environment variables, or logsRevoke all sessions via IAM deny policy; rotate secret; quarantine instance
UnauthorizedAccess:EC2/SSHBruteForceMedium (5.0)Brute-force SSH against MCP server host — common for internet-facing instancesVerify SSH key-only auth is enforced (no password); add security group rule limiting SSH to bastion
CryptoCurrency:Lambda/BitcoinToolHigh (7.5)Lambda MCP function making DNS queries to mining pools — likely code injection via user-controlled tool call payloadDisable function; review recent invocation logs; check if input sanitization passes user-controlled strings to shell/eval
Backdoor:EC2/C&CActivity.B!DNSHigh (8.0)MCP server host making DNS queries to known C2 domains — malware or backdoor installedIsolate instance via security group; capture memory dump; snapshot EBS; terminate and replace
Recon:EC2/PortProbeUnprotectedPortLow–Medium (2.0–5.0)External entity scanning open ports on MCP server — often a scanning bot, not targeted attackReview security groups for unnecessary open ports; if expected (monitoring tool), add suppression filter
UnauthorizedAccess:IAMUser/InstanceCredentialExfiltrationHigh (8.0)IAM user credentials (not role) used from AWS instance — API keys checked into MCP server codeDeactivate IAM user access key; audit code for hardcoded credentials; rotate key
Policy:IAMUser/RootCredentialUsageHigh (8.0)Root account credentials used — should never happen in productionImmediately review root activity in CloudTrail; lock root account with MFA; audit what root did

Failure modes reference

FailureSymptomFix
ListFindings returns 0 results despite known threatsNo findings in console or APICheck service.archived filter — default omits archived findings; also verify detector is ENABLED in the correct region
GetFindings returns empty Findings arrayListFindings returned IDs but GetFindings emptyFinding IDs expire after 90 days and return empty (not an error); also verify you're querying the correct detector ID and region
UpdateFindingsFeedback has no effect on future similar findingsSame false-positive pattern keeps generating findingsFeedback trains the ML model over time but doesn't suppress immediately; create a CreateFilter with ARCHIVE action for instant suppression
ArchiveFindings fails with ResourceNotFoundExceptionCannot archive specific finding IDsFinding may have already been archived (check service.archived), or finding ID is from a different detector/region
Deduplication causes missed escalationOngoing attack (count=1000) looks like a single findingCheck service.eventLastSeen in your remediation Lambda — if within the last 15 minutes, the threat is ongoing regardless of count