diff --git a/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml b/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml new file mode 100644 index 00000000000..7d67d748076 --- /dev/null +++ b/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml @@ -0,0 +1,213 @@ +Parser: + Title: Network Session ASIM parser for AWS GuardDuty findings + Version: '0.1.2' + LastUpdated: Jul 28, 2026 +Product: + Name: AWS GuardDuty +Normalization: + Schema: NetworkSession + Version: '0.2.6' +References: + - Title: ASIM Network Session Schema + Link: https://aka.ms/ASimNetworkSessionDoc + - Title: ASIM + Link: https://aka.ms/AboutASIM + - Title: Amazon GuardDuty finding types + Link: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html + - Title: Amazon GuardDuty finding format + Link: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-format.html +Description: | + This ASIM parser supports normalizing AWS GuardDuty findings that carry network context (service.action.networkConnectionAction or service.action.awsApiCallAction) to the ASIM Network Session normalized schema. Findings are ingested through the Microsoft Sentinel AWS S3 connector into the AWSGuardDuty table; findings without network context (for example, pure S3 policy or EKS control-plane findings) are filtered out by this parser and are better served by AssumedRole/S3/EKS-specific content. +ParserName: ASimNetworkSessionAWSGuardDuty +EquivalentBuiltInParser: _ASim_NetworkSession_AWSGuardDuty +ParserParams: + - Name: disabled + Type: bool + Default: false +ParserQuery: | + let parser = (disabled: bool = false) { + AWSGuardDuty + | where not(disabled) + // Only findings that carry network context are in scope for this parser. + // (DNS_REQUEST is intentionally excluded: GuardDuty's dnsRequestAction has no + // remoteIpDetails, so it cannot be normalized to a network session here.) + | extend ActionType_ = tostring(ServiceDetails.action.actionType) + | where ActionType_ in ("NETWORK_CONNECTION", "AWS_API_CALL", "PORT_PROBE") + | extend + NetworkAction_ = ServiceDetails.action.networkConnectionAction, + ApiCallAction_ = ServiceDetails.action.awsApiCallAction, + PortProbeAction_ = ServiceDetails.action.portProbeAction + // A port-probe finding can list several probed ports/remote IPs; expand so each probe + // becomes its own normalized session instead of silently keeping only the first. This is a + // no-op for other action types (PortProbeAction_ is null, so mv-expand yields one row unchanged). + | mv-expand PortProbeDetail_ = PortProbeAction_.portProbeDetails + // The IP GuardDuty flagged as the other party in the session (not yet Src/Dst - direction is resolved below). + | extend RemoteDetails_ = case( + ActionType_ == "NETWORK_CONNECTION", NetworkAction_.remoteIpDetails, + ActionType_ == "AWS_API_CALL", ApiCallAction_.remoteIpDetails, + ActionType_ == "PORT_PROBE", PortProbeDetail_.remoteIpDetails, + dynamic(null) + ) + | extend RemoteIp_ = tostring(RemoteDetails_.ipAddressV4) + | where isnotempty(RemoteIp_) + | extend + InstanceDetails_ = ResourceDetails.instanceDetails, + NetIf0_ = ResourceDetails.instanceDetails.networkInterfaces[0] + // Local (AWS-side) and remote port, and connection direction. AWS_API_CALL has no local + // network peer (it targets an AWS control-plane API, not a traceable endpoint), and + // PORT_PROBE is always effectively inbound - GuardDuty does not report a port for the prober. + | extend + LocalIp_ = coalesce(tostring(NetIf0_.privateIpAddress), tostring(NetIf0_.publicIp), ""), + LocalPort_ = case( + ActionType_ == "NETWORK_CONNECTION", toint(NetworkAction_.localPortDetails.port), + ActionType_ == "PORT_PROBE", toint(PortProbeDetail_.localPortDetails.port), + int(null) + ), + RemotePort_ = case( + ActionType_ == "NETWORK_CONNECTION", toint(NetworkAction_.remotePortDetails.port), + int(null) + ), + IsApiCall_ = ActionType_ == "AWS_API_CALL", + IsInbound_ = ActionType_ == "PORT_PROBE" + or (ActionType_ == "NETWORK_CONNECTION" and tostring(NetworkAction_.connectionDirection) == "INBOUND") + // ASIM event metadata + | extend + EventStartTime = TimeCreated, + EventEndTime = TimeCreated, + EventType = "NetworkSession", + EventSchema = "NetworkSession", + EventSchemaVersion = "0.2.6", + EventVendor = "AWS", + EventProduct = "GuardDuty", + EventProductVersion = SchemaVersion, + EventCount = toint(1), + EventResult = case( + ActionType_ == "PORT_PROBE", "Failure", + tobool(NetworkAction_.blocked) == true, "Failure", + "Success" + ), + EventSeverity = case( + Severity >= 7, "High", + Severity >= 4, "Medium", + Severity >= 1, "Low", + "Informational" + ), + EventOriginalSeverity = tostring(Severity), + EventMessage = Title, + EventOriginalType = ActivityType, + EventOriginalUid = Id, + EventUid = strcat("guardduty-", Id) + // Source/destination, resolved by direction. Inbound & port-probe: the remote party + // initiated, so it is Src and the AWS resource is Dst. Outbound: the reverse. AWS_API_CALL: + // the caller is always Src; there is no meaningful destination endpoint to report. + | extend + SrcIpAddr = case(IsApiCall_, RemoteIp_, IsInbound_, RemoteIp_, LocalIp_), + DstIpAddr = case(IsApiCall_, "", IsInbound_, LocalIp_, RemoteIp_), + SrcPortNumber = case(IsApiCall_, int(null), IsInbound_, RemotePort_, LocalPort_), + DstPortNumber = case(IsApiCall_, int(null), IsInbound_, LocalPort_, RemotePort_), + SrcHostname = case(IsApiCall_ or IsInbound_, "", tostring(InstanceDetails_.instanceId)), + DstHostname = case(not(IsApiCall_) and IsInbound_, tostring(InstanceDetails_.instanceId), ""), + SrcDvcId = case(IsApiCall_ or IsInbound_, "", tostring(InstanceDetails_.instanceId)), + DstDvcId = case(not(IsApiCall_) and IsInbound_, tostring(InstanceDetails_.instanceId), ""), + SrcDomain = case(IsApiCall_ or IsInbound_, "", tostring(NetIf0_.vpcId)), + DstDomain = case(not(IsApiCall_) and IsInbound_, tostring(NetIf0_.vpcId), ""), + SrcGeoCountry = case(IsApiCall_ or IsInbound_, tostring(RemoteDetails_.country.countryName), ""), + DstGeoCountry = case(not(IsApiCall_) and not(IsInbound_), tostring(RemoteDetails_.country.countryName), ""), + SrcGeoCity = case(IsApiCall_ or IsInbound_, tostring(RemoteDetails_.city.cityName), ""), + DstGeoCity = case(not(IsApiCall_) and not(IsInbound_), tostring(RemoteDetails_.city.cityName), ""), + SrcDescription = case(IsApiCall_ or IsInbound_, tostring(RemoteDetails_.organization.org), ""), + DstDescription = case(not(IsApiCall_) and not(IsInbound_), tostring(RemoteDetails_.organization.org), "") + | extend + SrcDvcIdType = iff(isnotempty(SrcDvcId), "Other", ""), + DstDvcIdType = iff(isnotempty(DstDvcId), "Other", ""), + SrcDomainType = iff(isnotempty(SrcDomain), "Other", ""), + DstDomainType = iff(isnotempty(DstDomain), "Other", "") + // Network fields. NetworkApplicationProtocol is derived from the destination port, + // i.e. whichever side holds the well-known service port once direction is resolved. + | extend + NetworkProtocol = toupper(tostring(coalesce(NetworkAction_.protocol, dynamic("")))), + // Derived from the same direction logic as Src/Dst above, not just connectionDirection, + // since NetworkAction_ (and so connectionDirection) is null for PORT_PROBE and AWS_API_CALL. + NetworkDirection = case( + IsApiCall_, "Unknown", + IsInbound_, "Inbound", + ActionType_ == "NETWORK_CONNECTION" and tostring(NetworkAction_.connectionDirection) == "OUTBOUND", "Outbound", + "Unknown" + ), + NetworkSessionId = Id, + // Not zero - GuardDuty does not report byte/packet counts, so these are unknown, not measured zeros. + NetworkBytes = long(null), + NetworkPackets = long(null), + NetworkApplicationProtocol = case( + DstPortNumber == 80, "HTTP", + DstPortNumber == 443, "HTTPS", + DstPortNumber == 53, "DNS", + DstPortNumber == 22, "SSH", + DstPortNumber == 3389, "RDP", + "" + ) + // Device (the EC2 instance or resource the finding is about, when present) + | extend + DvcId = tostring(InstanceDetails_.instanceId), + DvcIdType = iff(isnotempty(tostring(InstanceDetails_.instanceId)), "Other", ""), + DvcHostname = tostring(InstanceDetails_.instanceId), + DvcOs = case( + tostring(InstanceDetails_.platform) == "windows", "Windows", + isnotempty(tostring(InstanceDetails_.platform)), tostring(InstanceDetails_.platform), + "" + ), + // Only NETWORK_CONNECTION carries a "blocked" flag; defaulting AWS_API_CALL/PORT_PROBE + // to "Allow" here would be a fabricated result, not an observed one. + DvcAction = case( + ActionType_ == "NETWORK_CONNECTION", iff(tobool(NetworkAction_.blocked) == true, "Deny", "Allow"), + "" + ), + DvcInterface = tostring(NetIf0_.networkInterfaceId) + // Threat fields - always point at the remote party GuardDuty actually flagged, + // regardless of which ASIM side (Src/Dst) it landed on above. + | extend + ThreatName = ActivityType, + ThreatRiskLevel = toint(Severity * 10), + ThreatOriginalRiskLevel = tostring(Severity), + ThreatField = iff(IsApiCall_ or IsInbound_, "SrcIpAddr", "DstIpAddr"), + ThreatIpAddr = RemoteIp_, + ThreatIsActive = true, + ThreatFirstReportedTime = TimeCreated, + ThreatLastReportedTime = TimeGenerated + // Rule fields + | extend + RuleName = ActivityType, + RuleNumber = int(0), + RuleAction = "Alert" + // AWS-specific context, packed for ASIM AdditionalFields + | extend AdditionalFields = bag_pack( + "AwsAccountId", AccountId, + "AwsRegion", Region, + "AwsPartition", Partition, + "VpcId", tostring(NetIf0_.vpcId), + "SubnetId", tostring(NetIf0_.subnetId), + "InstanceId", tostring(InstanceDetails_.instanceId), + "InstanceType", tostring(InstanceDetails_.instanceType), + "GuardDutyFindingArn", Arn, + "GuardDutyActionType", ActionType_, + "RemoteAsn", tostring(RemoteDetails_.organization.asn), + "RemoteIsp", tostring(RemoteDetails_.organization.isp) + ) + | project + EventStartTime, EventEndTime, EventType, EventSchema, EventSchemaVersion, + EventVendor, EventProduct, EventProductVersion, EventCount, EventResult, + EventSeverity, EventOriginalSeverity, EventMessage, EventOriginalType, + EventOriginalUid, EventUid, + SrcIpAddr, SrcPortNumber, SrcHostname, SrcDvcId, SrcDvcIdType, SrcDomain, SrcDomainType, + SrcGeoCountry, SrcGeoCity, SrcDescription, + DstIpAddr, DstPortNumber, DstHostname, DstDvcId, DstDvcIdType, DstDomain, DstDomainType, + DstGeoCountry, DstGeoCity, DstDescription, + NetworkProtocol, NetworkDirection, NetworkSessionId, NetworkBytes, NetworkPackets, NetworkApplicationProtocol, + DvcId, DvcIdType, DvcHostname, DvcOs, DvcAction, DvcInterface, + ThreatName, ThreatRiskLevel, ThreatOriginalRiskLevel, ThreatField, ThreatIpAddr, + ThreatIsActive, ThreatFirstReportedTime, ThreatLastReportedTime, + RuleName, RuleNumber, RuleAction, + AdditionalFields, + TimeGenerated + }; + parser (disabled) diff --git a/Solutions/Amazon Web Services/Data/Solution_AmazonWebServices.json b/Solutions/Amazon Web Services/Data/Solution_AmazonWebServices.json index 07cb2b89a55..efd7769cffe 100644 --- a/Solutions/Amazon Web Services/Data/Solution_AmazonWebServices.json +++ b/Solutions/Amazon Web Services/Data/Solution_AmazonWebServices.json @@ -112,10 +112,13 @@ "Hunting Queries/AWS_STStoECS.yaml", "Hunting Queries/AWS_STStoGlue.yaml", "Hunting Queries/AWS_STStoKWN.yaml", - "Hunting Queries/AWS_STStoLambda.yaml" + "Hunting Queries/AWS_STStoLambda.yaml", + "Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml", + "Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml", + "Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml" ], "BasePath": "C:\\One\\Azure\\Azure-Sentinel\\Solutions\\Amazon Web Services", - "Version": "3.0.10", + "Version": "3.0.11", "Metadata": "SolutionMetadata.json", "TemplateSpec": true, "StaticDataConnectorIds": [ diff --git a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml new file mode 100644 index 00000000000..2550b973564 --- /dev/null +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml @@ -0,0 +1,54 @@ +id: 869d2742-80d8-4b72-89ad-af31ea6b7437 +name: AWS GuardDuty - EKS privilege escalation and credential access +description: | + Identifies AWS GuardDuty findings against EKS/Kubernetes resources that indicate privilege escalation, credential access, or defense-evasion activity in a cluster (privileged containers, sensitive host-path mounts, anonymous or unusual API-server access, and instance-credential exfiltration from a node). Groups by cluster and Kubernetes identity so a hunter can see which workload or user is repeatedly implicated. + Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-kubernetes.html +requiredDataConnectors: + - connectorId: AWSS3 + dataTypes: + - AWSGuardDuty +tactics: + - PrivilegeEscalation + - CredentialAccess + - Persistence +relevantTechniques: + - T1611 + - T1552 + - T1078 +query: | + AWSGuardDuty + | where ActivityType has "Kubernetes" + or ActivityType has_any ( + "PrivilegeEscalation:IAMUser/InstanceCredentialExfiltration", + "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration" + ) + | extend + ThreatPurpose = tostring(split(ActivityType, ":")[0]), + ClusterName = tostring(ResourceDetails.eksClusterDetails.name), + ClusterArn = tostring(ResourceDetails.eksClusterDetails.arn), + K8sUserName = tostring(ResourceDetails.kubernetesDetails.kubernetesUserDetails.username), + K8sWorkload = tostring(ResourceDetails.kubernetesDetails.kubernetesWorkloadDetails.name), + K8sNamespace = tostring(ResourceDetails.kubernetesDetails.kubernetesWorkloadDetails.namespace), + InstanceId = tostring(ResourceDetails.instanceDetails.instanceId), + CallerIp = coalesce( + tostring(ServiceDetails.action.kubernetesApiCallAction.sourceIps[0]), + tostring(ServiceDetails.action.awsApiCallAction.remoteIpDetails.ipAddressV4) + ) + | extend Identity = coalesce(K8sUserName, InstanceId, "unknown") + | summarize + FindingCount = count(), + FindingTypes = make_set(ActivityType, 10), + Workloads = make_set_if(K8sWorkload, isnotempty(K8sWorkload), 10), + Namespaces = make_set_if(K8sNamespace, isnotempty(K8sNamespace), 10), + CallerIps = make_set_if(CallerIp, isnotempty(CallerIp), 10), + MaxSeverity = max(Severity), + FirstSeen = min(TimeCreated), + LastSeen = max(TimeGenerated) + by AccountId, Region, ThreatPurpose, ClusterName, ClusterArn, Identity + | order by MaxSeverity desc, FindingCount desc +entityMappings: + - entityType: CloudApplication + fieldMappings: + - identifier: AppId + columnName: AccountId +version: 1.0.2 diff --git a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml new file mode 100644 index 00000000000..471f8520a93 --- /dev/null +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml @@ -0,0 +1,43 @@ +id: d9329491-ab19-4903-aa0f-7affb388b011 +name: AWS GuardDuty - High severity findings clustered by account and actor +description: | + Surfaces AWS GuardDuty findings with severity 7.0 or higher, grouped by AWS account, activity family and the underlying actor (instance, IAM principal, or access key) so a hunter can quickly spot an account or identity generating a disproportionate share of high-severity activity, rather than triaging findings one at a time. + Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity +requiredDataConnectors: + - connectorId: AWSS3 + dataTypes: + - AWSGuardDuty +tactics: + - Discovery + - InitialAccess +relevantTechniques: + - T1580 +query: | + AWSGuardDuty + | where Severity >= 7.0 + | extend + ThreatPurpose = tostring(split(ActivityType, ":")[0]), + ResourceType = tostring(ResourceDetails.resourceType), + InstanceId = tostring(ResourceDetails.instanceDetails.instanceId), + AccessKeyUserName = tostring(ResourceDetails.accessKeyDetails.userName), + RemoteIp = coalesce( + tostring(ServiceDetails.action.networkConnectionAction.remoteIpDetails.ipAddressV4), + tostring(ServiceDetails.action.awsApiCallAction.remoteIpDetails.ipAddressV4) + ) + | extend Actor = coalesce(AccessKeyUserName, InstanceId, "unknown") + | summarize + FindingCount = count(), + DistinctFindingTypes = dcount(ActivityType), + FindingTypes = make_set(ActivityType, 10), + RemoteIps = make_set_if(RemoteIp, isnotempty(RemoteIp), 10), + FirstSeen = min(TimeCreated), + LastSeen = max(TimeGenerated) + by AccountId, Region, ThreatPurpose, ResourceType, Actor + | where FindingCount >= 2 + | order by FindingCount desc +entityMappings: + - entityType: CloudApplication + fieldMappings: + - identifier: AppId + columnName: AccountId +version: 1.0.2 diff --git a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml new file mode 100644 index 00000000000..57050d53bc5 --- /dev/null +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml @@ -0,0 +1,47 @@ +id: 81ff8c3c-b7c1-40ff-9389-800fcf2d484e +name: AWS GuardDuty - Publicly exposed or weakly protected S3 buckets +description: | + Identifies AWS GuardDuty findings that flag an S3 bucket becoming publicly accessible, having public-access-block protections disabled, or showing anomalous discovery/exfiltration activity against its objects. Groups by bucket so a hunter can see the full sequence of policy-weakening and access events against a single bucket over the lookback period. + Reference: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-s3.html +requiredDataConnectors: + - connectorId: AWSS3 + dataTypes: + - AWSGuardDuty +tactics: + - Exfiltration + - Discovery + - DefenseEvasion +relevantTechniques: + - T1530 + - T1580 +query: | + AWSGuardDuty + | where ResourceDetails.resourceType == "S3Bucket" + and ActivityType has_any ("Policy:S3", "Exfiltration:S3", "Discovery:S3", "Impact:S3", "Stealth:S3") + | extend + ThreatPurpose = tostring(split(ActivityType, ":")[0]), + BucketName = tostring(ResourceDetails.s3BucketDetails[0].name), + BucketArn = tostring(ResourceDetails.s3BucketDetails[0].arn), + PublicAccessEffective = tostring(ResourceDetails.s3BucketDetails[0].publicAccess.effectivePermission), + ActorUserName = tostring(ResourceDetails.accessKeyDetails.userName), + CallerIp = tostring(ServiceDetails.action.awsApiCallAction.remoteIpDetails.ipAddressV4) + // A missing/empty s3BucketDetails array would otherwise collapse every unrelated finding + // without a bucket ARN into one bogus group. + | where isnotempty(BucketArn) + | summarize + FindingCount = count(), + FindingTypes = make_set(ActivityType, 10), + PublicAccessStates = make_set_if(PublicAccessEffective, isnotempty(PublicAccessEffective), 5), + Actors = make_set_if(ActorUserName, isnotempty(ActorUserName), 10), + CallerIps = make_set_if(CallerIp, isnotempty(CallerIp), 10), + MaxSeverity = max(Severity), + FirstSeen = min(TimeCreated), + LastSeen = max(TimeGenerated) + by AccountId, Region, ThreatPurpose, BucketName, BucketArn + | order by MaxSeverity desc, FindingCount desc +entityMappings: + - entityType: CloudApplication + fieldMappings: + - identifier: AppId + columnName: AccountId +version: 1.0.2 diff --git a/Solutions/Amazon Web Services/ReleaseNotes.md b/Solutions/Amazon Web Services/ReleaseNotes.md index 90578c02c37..1f47a9044f1 100644 --- a/Solutions/Amazon Web Services/ReleaseNotes.md +++ b/Solutions/Amazon Web Services/ReleaseNotes.md @@ -1,5 +1,6 @@ | **Version** | **Date Modified (DD-MM-YYYY)** | **Change History** | |-------------|--------------------------------|---------------------------------------------| +| 3.0.11 | 29-07-2026 | Added new AWS GuardDuty **Hunting Queries** (high-severity findings, EKS privilege escalation/credential access, S3 public exposure) | | 3.0.10 | 19-05-2026 | Added non-analytics tier queries to Amazon Web Services S3 **Data Connector** to support Basic/Auxiliary plan tables. | | 3.0.9 | 18-05-2026 | Update AWS **Hunting Queries** and **Workbooks** for Quality | | 3.0.8 | 13-01-2026 | Updated non-functional links from **Analytic rules** and **Hunting query** |