AWS Security Hub ASFF Schema: Fields, Severity, and Integration Guide

Retool's new app builder is where AI-generated code ships safely
Building apps with AI is easy. Getting them to production safely is another story.
If you have written a Lambda function to process Security Hub findings and it silently dropped half your events, you probably hit an ASFF validation error you never saw. The AWS Security Findings Format (ASFF) is not a loose JSON envelope: it is a strict schema with required fields, constrained enumerations, and a validation layer that rejects non-conforming payloads without surfacing a meaningful error to your pipeline.
Every finding that enters Security Hub: from GuardDuty, Inspector v2, Macie, AWS Config, and any third-party integration: is normalized into ASFF before it is stored or evaluated. That normalization is what makes cross-service correlation possible, but it also means your automation code must understand the exact field semantics, not just the field names.
This guide covers the actual ASFF field structure as it exists in production: the top-level required fields, the Severity object and its four subfields, the Resources array structure, the ProductFields map for vendor-specific data, the Workflow object status machine, and the most common errors that cause findings to be dropped or misrouted. Code examples use Python in Lambda because that is the dominant runtime for Security Hub event processing, but the field logic applies to any language.
The goal is to give you a working mental model of ASFF so that when you open a raw finding JSON from a GuardDuty event or a BatchImportFindings call, every field is immediately interpretable and you know exactly which ones your automation must read, write, or leave untouched.
Top-Level ASFF Structure: Required and Conditional Fields
A valid ASFF finding is a JSON object with 22 top-level fields defined in the schema. Of these, AWS requires a specific subset to be present for any finding to be accepted. The following fields are required for all findings submitted via BatchImportFindings:
AwsAccountId is the 12-digit AWS account number that owns the finding. CreatedAt and UpdatedAt are ISO 8601 timestamps with millisecond precision: misformatting these is one of the top rejection causes. Description is a free-text string limited to 1,024 characters. GeneratorId identifies the specific rule, check, or detector that produced the finding (for GuardDuty this is the threat intelligence rule ARN). Id is a provider-assigned unique identifier for the finding, which must be stable across updates to the same finding. ProductArn is the ARN of the product that generated the finding. Region is the AWS region string. Resources is an array of affected resources. SchemaVersion must be exactly "2018-10-08": the schema has not incremented since launch. Severity is the object that encodes finding severity. Title is limited to 256 characters. Types is an array of finding type strings drawn from the ASFF taxonomy.
Fields that appear in most findings but are not universally required include: FindingProviderFields (the provider's original severity and types before Security Hub normalization), ProductFields (a string-to-string map for vendor-specific data), RecordState (ACTIVE or ARCHIVED), and Workflow (the analyst workflow status object).
One architectural point that trips engineers up: Security Hub owns the Severity object at the hub level, but the original provider severity lives in FindingProviderFields.Severity. When you update a finding's severity through the console or API, Security Hub writes to Severity but preserves FindingProviderFields.Severity as the source-of-truth record from the generator. Your Lambda automations should read from Severity for routing decisions and from FindingProviderFields.Severity to understand what the originating tool reported.
Required fields for BatchImportFindings
AwsAccountId, CreatedAt, Description, GeneratorId, Id, ProductArn, Region, Resources, SchemaVersion (must be '2018-10-08'), Severity, Title, Types, UpdatedAt
Conditional/common fields
FindingProviderFields, ProductFields, RecordState, Workflow, Compliance, Remediation, Note, UserDefinedFields
Timestamp format requirement
CreatedAt and UpdatedAt must be ISO 8601 with milliseconds: '2026-06-25T14:30:00.000Z': not epoch integers, not date-only strings
The Severity Object: Label, Normalized, Original, and Product
The Severity field is the most misunderstood part of ASFF because it contains four subfields that encode severity in different ways for different consumers. Getting this wrong means your SIEM receives wrong severity data or your Lambda routes findings to the wrong queue.
Severity.Label is the canonical string enumeration for Security Hub's severity tier. Valid values are INFORMATIONAL, LOW, MEDIUM, HIGH, and CRITICAL. This is the field to use in all routing logic, alerting rules, and human-facing displays. Security Hub populates this field and normalizes it across all integrated products.
Severity.Normalized is an integer from 0 to 100 that maps to the Label tiers. The mapping is: 0 = INFORMATIONAL, 1-39 = LOW, 40-69 = MEDIUM, 70-89 = HIGH, 90-100 = CRITICAL. This field exists for legacy compatibility and for fine-grained ordering within a tier. A finding with Normalized=45 and one with Normalized=65 are both MEDIUM, but the 65 is closer to the HIGH boundary. Do not overwrite Normalized when importing custom findings unless you have a precise value: an incorrect Normalized that crosses a tier boundary will override the Label you set.
Severity.Original is a string field that preserves the severity rating from the originating tool in its native format. GuardDuty stores values like "5" or "8.5" here. Inspector v2 stores the CVSS v3 base score as a string. Macie stores its own internal severity label. This field is read-only from Security Hub's perspective: it is set by the provider and should not be modified.
Severity.Product is a numeric field (0-100 scale) representing the provider's own normalized score before Security Hub mapping. For custom integrations using BatchImportFindings, if you only set Severity.Label, Security Hub infers a midpoint Normalized value for that tier. If you also set Severity.Normalized to a specific value, it takes precedence for ordering within the tier.
Briefings like this, every morning before 9am.
Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.
Resources Array: Type, Id, Partition, Region, and Details
The Resources field is an array of objects, each representing an AWS resource affected by the finding. A finding can reference multiple resources, but most findings target a single primary resource. Each resource object has four required fields and one optional field that contains resource-specific detail.
Type is a string drawn from the ASFF resource type taxonomy. Examples include AwsEc2Instance, AwsS3Bucket, AwsIamRole, AwsLambdaFunction, AwsRdsDbInstance, and AwsEksCluster. The full list covers approximately 70 resource types. Using a type string not in the taxonomy causes the finding to be rejected during import.
Id is the ARN or unique identifier for the resource. For EC2 instances this is the instance ARN. For S3 buckets this is the bucket ARN or name. For IAM entities this is the entity ARN. Security Hub uses this field for resource-based grouping and deduplication. Malformed or missing ARNs are a common source of dropped findings.
Partition is the AWS partition: "aws", "aws-cn" for China regions, or "aws-us-gov" for GovCloud. Region is the AWS region where the resource exists, which may differ from the finding's top-level Region if the finding is about a global resource evaluated in a specific region.
Details is an optional nested object keyed by resource type. For example, a finding about an EC2 instance would have Details.AwsEc2Instance containing fields like ImageId, IpV4Addresses, KeyName, LaunchDate, SubnetId, Type, and VpcId. For custom findings, populate the Details object with whatever resource-type-specific data is available. Leaving Details empty is valid but reduces the value of the finding for downstream consumers who want to correlate on resource attributes.
Resource object required fields
Type (from ASFF taxonomy), Id (ARN or unique identifier), Partition ('aws', 'aws-cn', 'aws-us-gov'), Region (resource's region)
Details schema
Optional nested object keyed by resource type (e.g., Details.AwsEc2Instance) containing resource-specific structured attributes
Common rejection cause
Using a resource Type string not in the ASFF taxonomy or providing a malformed Id that does not match ARN format for the resource type
ProductFields and the Workflow Object
ProductFields is a flat string-to-string map that integrations use to carry vendor-specific metadata that does not fit the standard ASFF schema. GuardDuty uses ProductFields to store fields like aws/guardduty/service/action/networkConnectionAction/connectionDirection and aws/securityhub/FindingId. The map is limited to 50 key-value pairs, with each key limited to 128 characters and each value limited to 2,048 characters. Keys must not start with "aws/" unless you are an AWS native service: use a reverse-domain prefix for custom fields: "mycompany/scanner/ruleId" is valid.
The Workflow object encodes the analyst triage state of a finding, completely separate from the finding's RecordState. Workflow has a single field: Status, which accepts four values. NEW is the initial state for all inbound findings. NOTIFIED means an alert has been sent but the finding is not yet resolved. SUPPRESSED means the finding has been acknowledged and intentionally ignored. RESOLVED means the underlying issue has been remediated.
RecordState is different from Workflow.Status. RecordState reflects whether the finding is still active from the provider's perspective: ACTIVE means the provider still considers the issue present; ARCHIVED means the provider has determined the issue is no longer present. A finding can be RecordState ACTIVE and Workflow RESOLVED simultaneously: the provider still sees the issue, but an analyst has marked it remediated in the hub.
Lambda functions that dedup findings must account for both fields. A finding that re-activates (RecordState flips from ARCHIVED back to ACTIVE) should reset Workflow.Status to NEW so analysts re-triage it.
ProductFields key constraints
Max 50 pairs; keys max 128 chars; values max 2,048 chars; keys must not start with 'aws/' for custom integrations
Workflow.Status values
NEW (default), NOTIFIED (alerted, pending), SUPPRESSED (acknowledged/ignored), RESOLVED (remediated)
RecordState vs Workflow.Status
RecordState reflects provider determination (ACTIVE/ARCHIVED); Workflow.Status reflects analyst triage state: they are independent fields
Writing a Lambda That Processes ASFF Findings Correctly
Security Hub delivers findings to Lambda via EventBridge rules. The event payload wraps the ASFF finding under detail.findings as an array. Always iterate the array: batch delivery means multiple findings can arrive in a single event.
The correct pattern for severity routing and finding type filtering in Python:
import json
def handler(event, context):
findings = event.get("detail", {}).get("findings", [])
for finding in findings:
severity_label = finding.get("Severity", {}).get("Label", "INFORMATIONAL")
record_state = finding.get("RecordState", "ACTIVE")
workflow_status = finding.get("Workflow", {}).get("Status", "NEW")
# Skip archived or already-resolved findings
if record_state == "ARCHIVED" or workflow_status in ("SUPPRESSED", "RESOLVED"):
continue
if severity_label in ("CRITICAL", "HIGH"):
route_to_pagerduty(finding)
elif severity_label == "MEDIUM":
route_to_jira(finding)
# Use composite key for dedup, not finding Id alone
resources = finding.get("Resources", [])
if resources:
product_arn = finding.get("ProductArn", "")
finding_id = finding.get("Id", "")
dedup_key = f"{product_arn}#{finding_id}"
Three mistakes to avoid: First, never use finding["Id"] as a dedup key across providers: Id is only unique within a provider's namespace. Use a composite of ProductArn plus Id. Second, do not write back to Severity.Normalized when calling BatchUpdateFindings: that API only accepts Severity.Label. Third, when filtering by finding type, use a prefix match because Types is an array with a hierarchical string format like "Software and Configuration Checks/Vulnerabilities/CVE": exact equality matching misses subtype variants.
Always log the FailedFindings array from every BatchImportFindings call. Every dropped finding is a gap in your security visibility that you will not discover until an incident review.
Mapping CVSS Scores and Custom Severity to ASFF
Third-party scanners and custom tooling typically produce CVSS v3 base scores on a 0-10 scale. Mapping these to ASFF Severity requires a defined translation table. The standard mapping used by AWS Inspector v2 and most approved integrations is:
CVSS 0.0 maps to Label INFORMATIONAL, Normalized 0. CVSS 0.1-3.9 maps to Label LOW, Normalized value is CVSS score multiplied by 10 (practical approximation). CVSS 4.0-6.9 maps to Label MEDIUM, Normalized 40-69. CVSS 7.0-8.9 maps to Label HIGH, Normalized 70-89. CVSS 9.0-10.0 maps to Label CRITICAL, Normalized 90-100.
A practical approximation used in many production integrations is to multiply the CVSS score by 10 and then assign Label based on the resulting Normalized value using the tier boundaries. This produces a Normalized of 70 for CVSS 7.0 and 90 for CVSS 9.0, which aligns with the tier thresholds.
Store the original CVSS score in Severity.Original as a string ("7.5") and in ProductFields with a key like "scanner/cvssV3BaseScore". This preserves the source data for consumers who need the raw score without losing the normalized ASFF representation.
For proprietary severity scores (LOW/MEDIUM/HIGH strings from internal tools), map directly to the corresponding ASFF Label and use the midpoint Normalized value for that tier: LOW maps to Normalized 20, MEDIUM to 55, HIGH to 80, CRITICAL to 95. Avoid setting Normalized to exact tier boundary values (1, 40, 70, 90) because some Security Hub UIs display boundary findings ambiguously.
The bottom line
ASFF is a strict schema, not a flexible envelope. Engineers who treat it as loosely typed JSON will encounter silent validation failures, severity misrouting, and dedup collisions. The fields that matter most for automation are Severity.Label for routing, RecordState and Workflow.Status together for lifecycle management, Resources[0].Id for resource correlation, and ProductFields for preserving vendor-specific data. Log the FailedFindings array from every BatchImportFindings call. Every dropped finding is a gap in your security visibility that you will not discover until an incident review.
Frequently asked questions
What is the ASFF schema in AWS Security Hub?
The AWS Security Findings Format (ASFF) is the JSON schema that Security Hub uses to standardize findings from all sources: GuardDuty, Inspector, Macie, Config, and third-party integrations. Every finding that enters Security Hub is validated against this schema. Required fields include AwsAccountId, CreatedAt, Description, GeneratorId, Id, ProductArn, Region, Resources, SchemaVersion, Severity, Title, Types, and UpdatedAt. Findings that fail validation are rejected and logged in the BatchImportFindings FailedFindings response.
How does AWS Security Hub severity work?
Security Hub severity is encoded in the Severity object, which contains four subfields: Label (the string tier: INFORMATIONAL, LOW, MEDIUM, HIGH, CRITICAL), Normalized (an integer 0-100 mapping to the tier ranges), Original (the source tool's native severity rating as a string), and Product (the provider's pre-normalization numeric score). Label is the authoritative field for routing and alerting. Normalized allows fine-grained ordering within a tier.
What is Severity.Normalized vs Severity.Label?
Severity.Label is the enumerated tier string (INFORMATIONAL, LOW, MEDIUM, HIGH, CRITICAL) used for human-readable display and routing logic. Severity.Normalized is an integer from 0 to 100 that provides numeric ordering within and across tiers. The tier boundaries are: 0 = INFORMATIONAL, 1-39 = LOW, 40-69 = MEDIUM, 70-89 = HIGH, 90-100 = CRITICAL. Use Label for routing decisions. Use Normalized only when you need to order or rank findings within the same tier.
How do I import custom findings into AWS Security Hub?
Use the BatchImportFindings API with your integration's ProductArn. Your product must be registered as an integration in Security Hub first. Each finding must conform to ASFF and include all required fields. SchemaVersion must be exactly '2018-10-08'. Timestamps must use ISO 8601 with milliseconds. Resource Type values must come from the ASFF resource type taxonomy. Always check the FailedFindings array in the API response: rejected findings are listed there with error codes, not raised as exceptions.
What ASFF fields does GuardDuty populate?
GuardDuty populates the full required ASFF set plus several additional fields. ProductFields contains GuardDuty-specific data including network connection direction, action type, and the original GuardDuty finding ID prefixed with 'aws/guardduty/'. FindingProviderFields.Severity carries GuardDuty's original severity score. Resources maps to the affected EC2 instance, IAM role, or S3 bucket with a populated Details subobject. Types uses GuardDuty's threat intelligence taxonomy strings.
Why are my custom ASFF findings being rejected?
The most common rejection causes are: timestamp format errors (CreatedAt and UpdatedAt must include milliseconds in ISO 8601 format), invalid SchemaVersion (must be exactly '2018-10-08'), Resource.Type not in the ASFF taxonomy, Description exceeding 1,024 characters, Title exceeding 256 characters, ProductArn ARN format violations, ProductFields keys starting with 'aws/' in a custom integration, and Severity.Normalized set to a value inconsistent with the specified Label tier. Always log the FailedFindings array from BatchImportFindings to diagnose rejections.
How do I map CVSS scores to ASFF severity?
Map CVSS v3 base scores to ASFF using these tier boundaries: 0.0 = INFORMATIONAL, 0.1-3.9 = LOW, 4.0-6.9 = MEDIUM, 7.0-8.9 = HIGH, 9.0-10.0 = CRITICAL. For Normalized, multiply the CVSS score by 10 as a practical approximation (CVSS 7.5 becomes Normalized 75, which falls in the HIGH tier). Store the original CVSS score as a string in Severity.Original and in ProductFields under a vendor-prefixed key.
What is the difference between RecordState and Workflow.Status in ASFF?
RecordState reflects the finding provider's determination of whether the issue is still present: ACTIVE means the provider sees the issue as ongoing; ARCHIVED means the provider considers it resolved. Workflow.Status reflects the analyst's triage state within Security Hub: NEW, NOTIFIED, SUPPRESSED, or RESOLVED. The two fields are independent. A finding can be RecordState ACTIVE and Workflow RESOLVED simultaneously. Lambda processors should check both fields to correctly handle finding lifecycle transitions.
Sources & references
Free resources
Critical CVE Reference Card 2025–2026
25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.
Ransomware Incident Response Playbook
Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.
Get threat intel before your inbox does.
50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.
Unsubscribe anytime. We never sell your data.

Founder & Cybersecurity Evangelist, Decryption Digest
Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.
Win a $2,495 Black Hat pass.
Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.
