Guide · AWS Security Hub · Findings API
AWS Security Hub Findings API — ASFF Format, BatchImportFindings, Workflow States
All Security Hub findings — whether from GuardDuty, Inspector, your own tooling, or a third-party integration — are represented in the AWS Security Finding Format (ASFF), a normalized JSON schema with mandatory and optional fields. For MCP server operators the critical distinction is between BatchImportFindings (used by integration sources to create or update findings) and BatchUpdateFindings (used by your team to update workflow state and annotations). These two APIs are deliberately separate: BatchImportFindings can only be called with a product ARN you own, and it can modify finding content fields. BatchUpdateFindings is for consumers — it can only update the workflow-related overlay fields (Workflow.Status, Note, Severity override, UserDefinedFields) and cannot change the underlying finding data that the source submitted. Understanding this separation prevents confusion when building automated triage or enrichment pipelines.
TL;DR
ASFF requires eight mandatory fields: SchemaVersion, Id, ProductArn, GeneratorId, AwsAccountId, Types, CreatedAt, UpdatedAt, Severity, and Resources. Use BatchImportFindings (max 100/call) to push custom security findings from MCP server health checks or external scanners. Use BatchUpdateFindings to set workflow state (NEW → RESOLVED/SUPPRESSED) and add analyst notes. Filter findings with GetFindings using ASFF field comparators. See Security Hub setup guide, compliance standards guide, and automation rules guide for related operations.
ASFF mandatory fields
Every finding — whether generated by AWS, a partner, or your own code — must contain these fields. Missing any one of them causes BatchImportFindings to reject the entire batch.
# Minimal valid ASFF finding for a custom MCP server security event
{
"SchemaVersion": "2018-10-08", # Fixed string — always exactly this value
"Id": "arn:aws:securityhub:us-east-1:123456789012:product/123456789012/default/mcp-server-health/ec2-i-1234abcd/endpoint-down-2026-10-08T10:00:00Z",
# Must be globally unique for this product; include timestamp + resource ID
"ProductArn": "arn:aws:securityhub:us-east-1:123456789012:product/123456789012/default",
# Your custom product ARN — created once, reused for all findings
"GeneratorId": "alivemcp-endpoint-health-checker",
# Identifies the tool/rule that generated the finding
"AwsAccountId": "123456789012", # Account where the affected resource lives
"Types": ["Software and Configuration Checks/Vulnerabilities/CVE"],
# Namespace/Category/Classifier — see types taxonomy below
"CreatedAt": "2026-10-08T10:00:00.000Z", # ISO 8601, when the finding was first observed
"UpdatedAt": "2026-10-08T10:00:00.000Z", # ISO 8601, when last updated (same as CreatedAt on first submit)
"Severity": {
"Label": "HIGH", # CRITICAL | HIGH | MEDIUM | LOW | INFORMATIONAL
"Normalized": 70 # 0-100; Label is derived from this: 76-100=CRITICAL, 51-75=HIGH, 26-50=MEDIUM, 1-25=LOW, 0=INFORMATIONAL
},
"Title": "MCP server endpoint unreachable for 15+ minutes",
"Description": "The MCP server endpoint https://example.com/mcp has not responded to health checks for 15 consecutive minutes. Last successful response: 2026-10-08T09:44:00Z.",
"Resources": [
{
"Type": "AwsEc2Instance", # Resource type — must match ASFF resource type taxonomy
"Id": "arn:aws:ec2:us-east-1:123456789012:instance/i-1234abcd5678efgh",
"Region": "us-east-1",
"Tags": {
"Service": "mcp-server",
"Environment": "production"
}
}
]
}
The Id field acts as a deduplication key. If you submit a finding with the same Id and ProductArn that already exists, Security Hub updates the existing finding rather than creating a new one — similar to GuardDuty's finding deduplication behavior. Build your Id to encode the specific resource + finding condition so the same ongoing problem updates one finding rather than flooding with duplicates.
Severity normalization across sources
Different services use different severity scales. Security Hub normalizes everything to both a Normalized integer (0–100) and a Label string. When GuardDuty sends a finding with severity 8.5, Security Hub maps it to Normalized 85, Label CRITICAL.
| Label | Normalized range | GuardDuty severity range | Inspector severity | Typical MCP action |
|---|---|---|---|---|
| CRITICAL | 76–100 | N/A (GuardDuty max is 8.9 → HIGH) | 9.0–10.0 CVSS | Page on-call immediately; isolate resource |
| HIGH | 51–75 | 7.0–8.9 | 7.0–8.9 CVSS | Create incident ticket within 1 hour |
| MEDIUM | 26–50 | 4.0–6.9 | 4.0–6.9 CVSS | Create ticket within 4 hours |
| LOW | 1–25 | 0.1–3.9 | 0.1–3.9 CVSS | Review in weekly triage |
| INFORMATIONAL | 0 | N/A | N/A | Log only; no action required |
# GuardDuty to Security Hub severity mapping example:
# GuardDuty finding: severity = 7.5 (HIGH in GuardDuty terms)
# In Security Hub ASFF:
# Severity.Original = "7.5"
# Severity.Product = 75 (GuardDuty maps 7.5 → 75 before sending to Security Hub)
# Severity.Normalized = 75 (Security Hub accepts Product value)
# Severity.Label = "HIGH" (75 falls in 51-75 HIGH range)
# Inspector CVSSv3 to Security Hub:
# Inspector CRITICAL (9.2 CVSS) → Severity.Normalized = 92, Label = CRITICAL
# For custom findings: set both Label and Normalized consistently
# Normalized = 70 → Label should be "HIGH" (51-75 range)
# Mismatched Label/Normalized causes confusion in dashboards — always keep them aligned
BatchImportFindings — sending custom findings
Use BatchImportFindings to push findings from your own security tools or MCP server health checks into Security Hub. You must first create a custom product ARN (one per AWS account — a single product ARN is reused for all findings from that account's tooling).
# Step 1: Enable your custom product integration in Security Hub
# This creates the product ARN your findings will use
aws securityhub enable-import-findings-for-product \
--product-subscription-arn "arn:aws:securityhub:us-east-1:123456789012:product/123456789012/default"
# Your custom product ARN format: arn:aws:securityhub:REGION:ACCOUNT:product/ACCOUNT/default
# Step 2: Send a custom finding
aws securityhub batch-import-findings \
--findings '[
{
"SchemaVersion": "2018-10-08",
"Id": "mcp-endpoint-down/ec2-i-1234abcd/2026-10-08T10:00:00Z",
"ProductArn": "arn:aws:securityhub:us-east-1:123456789012:product/123456789012/default",
"GeneratorId": "alivemcp-health-checker-v1",
"AwsAccountId": "123456789012",
"Types": ["Software and Configuration Checks/Industry and Regulatory Standards"],
"CreatedAt": "2026-10-08T10:00:00.000Z",
"UpdatedAt": "2026-10-08T10:00:00.000Z",
"Severity": {"Label": "HIGH", "Normalized": 70},
"Title": "MCP server endpoint unreachable — 15+ minutes downtime",
"Description": "AliveMCP health check: endpoint https://example.com/mcp returned no response for 15 consecutive checks (check interval: 60s). Last success: 2026-10-08T09:44:00Z. Consecutive failures: 15.",
"SourceUrl": "https://alivemcp.com/status/example-mcp-server",
"Resources": [{
"Type": "AwsEc2Instance",
"Id": "arn:aws:ec2:us-east-1:123456789012:instance/i-1234abcd5678efgh",
"Region": "us-east-1",
"Tags": {"Service": "mcp-server", "Environment": "production"}
}],
"Remediation": {
"Recommendation": {
"Text": "Check MCP server process status and network connectivity. Review CloudWatch logs for OOM or crash signals.",
"Url": "https://docs.aws.amazon.com/ecs/latest/developerguide/stopped-task-errors.html"
}
},
"UserDefinedFields": {
"endpoint": "https://example.com/mcp",
"consecutive_failures": "15",
"last_success": "2026-10-08T09:44:00Z"
}
}
]'
# Output: {"FailedCount": 0, "SuccessCount": 1, "FailedFindings": []}
# FailedFindings contains rejection reasons if any finding is invalid
Rate limits: custom product BatchImportFindings is limited to 10,000 findings per month in the free tier, then $0.00003/finding. For continuous MCP server health monitoring sending findings at high frequency, consider batching — send one finding update when the server first goes down (CreatedAt = downtime start) and update the same finding ID (UpdatedAt = now, adding context to Description) on each subsequent check interval rather than creating a new finding per check cycle.
BatchUpdateFindings — workflow management
BatchUpdateFindings is the API for consumers (your team, your triage automation) to update the overlay fields on findings without changing the underlying finding data from the source. You cannot use this API to change Title, Description, Severity.Normalized from the source — only the workflow overlay.
# Resolve a finding after the MCP server comes back online
aws securityhub batch-update-findings \
--finding-identifiers '[{
"Id": "mcp-endpoint-down/ec2-i-1234abcd/2026-10-08T10:00:00Z",
"ProductArn": "arn:aws:securityhub:us-east-1:123456789012:product/123456789012/default"
}]' \
--workflow '{"Status": "RESOLVED"}' \
--note '{"Text": "MCP server recovered at 10:47 UTC. Root cause: ECS task OOM — memory limit increased.", "UpdatedBy": "ops-automation"}'
# Suppress a known false positive (won't reappear in active findings view)
aws securityhub batch-update-findings \
--finding-identifiers '[{
"Id": "arn:aws:securityhub:us-east-1:123456789012:subscription/aws-foundational-security-best-practices/v/1.0.0/S3.4/finding/abc123",
"ProductArn": "arn:aws:securityhub:us-east-1::product/aws/securityhub"
}]' \
--workflow '{"Status": "SUPPRESSED"}' \
--note '{"Text": "S3 bucket intentionally public for static assets — approved by security review 2026-10-01", "UpdatedBy": "security-team"}'
# Override severity for a finding (escalate LOW to HIGH for production environment)
aws securityhub batch-update-findings \
--finding-identifiers '[{"Id": "...", "ProductArn": "..."}]' \
--severity '{"Label": "HIGH"}' \
--note '{"Text": "Severity escalated: affects production MCP endpoint serving 1000+ requests/day", "UpdatedBy": "ops"}'
# Max 100 finding identifiers per BatchUpdateFindings call
# Workflow states: NEW | NOTIFIED | RESOLVED | SUPPRESSED
# NEW: default state for all incoming findings
# NOTIFIED: ticket created / team informed
# RESOLVED: finding fixed
# SUPPRESSED: accepted risk or false positive — hidden from default views
GetFindings — filtering and querying
The GetFindings API uses filter objects with comparison operators. Each filter field supports string match, prefix match, date range, and number range comparators depending on field type.
# Get all HIGH+ severity findings in NEW state from GuardDuty
aws securityhub get-findings \
--filters '{
"SeverityLabel": [{"Value": "HIGH", "Comparison": "EQUALS"}, {"Value": "CRITICAL", "Comparison": "EQUALS"}],
"WorkflowStatus": [{"Value": "NEW", "Comparison": "EQUALS"}],
"ProductName": [{"Value": "GuardDuty", "Comparison": "EQUALS"}],
"RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}]
}' \
--sort-criteria '[{"Field": "updatedAt", "SortOrder": "desc"}]' \
--max-results 100
# Multiple values for same field = OR; different fields = AND
# Above query: (severity=HIGH OR severity=CRITICAL) AND workflow=NEW AND product=GuardDuty AND state=ACTIVE
# Get findings for a specific AWS resource
aws securityhub get-findings \
--filters '{
"ResourceId": [{"Value": "arn:aws:ec2:us-east-1:123456789012:instance/i-1234abcd5678efgh", "Comparison": "EQUALS"}]
}'
# Get findings created in the last 24 hours
aws securityhub get-findings \
--filters '{
"CreatedAt": [{"Start": "2026-10-07T00:00:00Z", "End": "2026-10-08T23:59:59Z", "DateRange": null}]
}'
# Get findings by type prefix (all findings in the TTPs namespace)
aws securityhub get-findings \
--filters '{
"Type": [{"Value": "TTPs", "Comparison": "PREFIX"}]
}'
# Paginate: NextToken returned when more results available
aws securityhub get-findings \
--filters '{"RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}]}' \
--max-results 100 \
--next-token "token-from-previous-response"
RecordState (ACTIVE/ARCHIVED) is distinct from Workflow.Status (NEW/NOTIFIED/RESOLVED/SUPPRESSED). A finding can be ACTIVE + RESOLVED (team resolved it but the source hasn't archived it yet) or ARCHIVED + NEW (source archived it but team hasn't acknowledged). Automation rules operate on both ACTIVE and ARCHIVED findings. The default Security Hub console view shows ACTIVE + (NEW or NOTIFIED) — explicitly set both filters when querying to match what the console shows.
Finding types taxonomy
The Types field uses a three-level hierarchy: Namespace/Category/Classifier. Using the correct namespace makes findings discoverable via type-based filters and groups them correctly in Security Hub's findings view.
| Namespace | When to use for MCP | Example Types value |
|---|---|---|
| TTPs | Attack techniques observed against your MCP server (MITRE ATT&CK) | TTPs/Initial Access/ExploitPublicFacingApplication |
| Software and Configuration Checks | Config compliance issues, CVEs in MCP dependencies, missing patches | Software and Configuration Checks/Vulnerabilities/CVE |
| Sensitive Data Identifications | Secrets or API keys found in MCP server logs or S3 buckets | Sensitive Data Identifications/PII/Credentials |
| Unusual Behaviors | Anomalous MCP endpoint usage patterns, unusual API call volumes | Unusual Behaviors/Application/NetworkTrafficVolumeAnomaly |
| Effects | Observed outcomes of a compromise (data exfiltration, resource abuse) | Effects/Data Exfiltration/NetworkDataExfiltration |
Failure modes reference
| Failure | Symptom | Fix |
|---|---|---|
| BatchImportFindings returns AccessDeniedException | Custom findings rejected with access error | ProductArn in the finding does not match the product ARN associated with calling IAM principal; verify product ARN is for the calling account and region |
| Finding ID collision — unexpected update instead of create | New finding overwrites an older one you wanted to keep | Id is identical to an existing finding for the same ProductArn; make Id unique by including resource ID + timestamp + condition type |
| BatchUpdateFindings returns InvalidAccessException | Cannot update workflow state on a finding | ProductArn in FindingIdentifiers references a product the caller doesn't have access to; BatchUpdateFindings is for any caller but requires correct ProductArn |
| GetFindings returns empty results despite active findings | Dashboard shows findings; API returns empty | Missing RecordState filter — default API behavior may differ from console default; add RecordState ACTIVE filter explicitly |
| Severity.Label mismatch warning in console | Finding shows as inconsistent severity | Severity.Normalized and Severity.Label don't agree (e.g., Normalized=70 but Label="CRITICAL"); keep them consistent — 70 = HIGH range (51-75) |
| Custom findings not appearing in Security Hub | BatchImportFindings returns SuccessCount=1 but finding not visible | Finding may be filtered by default console view; check RecordState=ACTIVE and WorkflowStatus=NEW filters; also verify the custom product integration is enabled |