From f7467c2f1a2f845e6a09ee914f7b17a8cb48b44c Mon Sep 17 00:00:00 2001 From: OluOlus Date: Mon, 27 Jul 2026 23:47:11 +0100 Subject: [PATCH 1/4] Add ASIM Network Session parser and hunting queries for AWS GuardDuty Adds ASimNetworkSessionAWSGuardDuty, querying the AWSGuardDuty table directly with no dependency on any private/unsubmitted function, plus three hunting queries: high-severity findings clustered by account and actor, EKS privilege escalation/credential access, and public or weakly-protected S3 buckets. --- .../ASimNetworkSessionAWSGuardDuty.yaml | 169 ++++++++++++++++++ ...KSPrivilegeEscalationCredentialAccess.yaml | 58 ++++++ ...Duty_HighSeverityFindingsCrossAccount.yaml | 47 +++++ .../AWS_GuardDuty_S3PublicExposure.yaml | 47 +++++ 4 files changed, 321 insertions(+) create mode 100644 Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml create mode 100644 Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml create mode 100644 Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml create mode 100644 Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml diff --git a/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml b/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml new file mode 100644 index 00000000000..e946ec6f15e --- /dev/null +++ b/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml @@ -0,0 +1,169 @@ +Parser: + Title: Network Session ASIM parser for AWS GuardDuty findings + Version: '0.1.0' + LastUpdated: Jul 27, 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. + | extend ActionType_ = tostring(ServiceDetails.action.actionType) + | where ActionType_ in ("NETWORK_CONNECTION", "AWS_API_CALL", "PORT_PROBE", "DNS_REQUEST") + | extend + NetworkAction_ = ServiceDetails.action.networkConnectionAction, + ApiCallAction_ = ServiceDetails.action.awsApiCallAction, + PortProbeAction_ = ServiceDetails.action.portProbeAction, + DnsAction_ = ServiceDetails.action.dnsRequestAction + // Prefer the network-connection view; fall back to the API-call caller IP for IAM-driven findings. + | extend RemoteDetails_ = case( + ActionType_ == "NETWORK_CONNECTION", NetworkAction_.remoteIpDetails, + ActionType_ == "AWS_API_CALL", ApiCallAction_.remoteIpDetails, + ActionType_ == "PORT_PROBE", PortProbeAction_.portProbeDetails[0].remoteIpDetails, + dynamic(null) + ) + | extend DstIpAddr_ = tostring(RemoteDetails_.ipAddressV4) + | where isnotempty(DstIpAddr_) + | extend + InstanceDetails_ = ResourceDetails.instanceDetails, + NetIf0_ = ResourceDetails.instanceDetails.networkInterfaces[0] + // ASIM event metadata + | extend + EventStartTime = TimeGenerated, + EventEndTime = TimeGenerated, + 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 (the AWS resource the finding is about) + | extend + SrcIpAddr = coalesce(tostring(NetIf0_.privateIpAddress), tostring(NetIf0_.publicIp), ""), + SrcPortNumber = toint(NetworkAction_.localPortDetails.port), + SrcHostname = tostring(InstanceDetails_.instanceId), + SrcDvcId = tostring(InstanceDetails_.instanceId), + SrcDvcIdType = iff(isnotempty(tostring(InstanceDetails_.instanceId)), "Other", ""), + SrcDomain = tostring(NetIf0_.vpcId), + SrcDomainType = iff(isnotempty(tostring(NetIf0_.vpcId)), "Other", "") + // Destination (the remote endpoint GuardDuty observed) + | extend + DstIpAddr = DstIpAddr_, + DstPortNumber = toint(coalesce(NetworkAction_.remotePortDetails.port, dynamic(null))), + DstGeoCountry = tostring(RemoteDetails_.country.countryName), + DstGeoCity = tostring(RemoteDetails_.city.cityName), + DstDescription = tostring(RemoteDetails_.organization.org) + // Network fields + | extend + NetworkProtocol = toupper(tostring(coalesce(NetworkAction_.protocol, dynamic("")))), + NetworkDirection = case( + tostring(NetworkAction_.connectionDirection) == "INBOUND", "Inbound", + tostring(NetworkAction_.connectionDirection) == "OUTBOUND", "Outbound", + "Unknown" + ), + NetworkSessionId = Id, + NetworkBytes = int(0), + NetworkPackets = int(0), + NetworkApplicationProtocol = case( + toint(NetworkAction_.remotePortDetails.port) == 80, "HTTP", + toint(NetworkAction_.remotePortDetails.port) == 443, "HTTPS", + toint(NetworkAction_.remotePortDetails.port) == 53, "DNS", + toint(NetworkAction_.remotePortDetails.port) == 22, "SSH", + toint(NetworkAction_.remotePortDetails.port) == 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), + "" + ), + DvcAction = iff(tobool(NetworkAction_.blocked) == true, "Deny", "Allow"), + DvcInterface = tostring(NetIf0_.networkInterfaceId) + // Threat fields + | extend + ThreatName = ActivityType, + ThreatRiskLevel = toint(Severity * 10), + ThreatOriginalRiskLevel = tostring(Severity), + ThreatField = "DstIpAddr", + ThreatIpAddr = DstIpAddr, + 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, + DstIpAddr, DstPortNumber, 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/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml new file mode 100644 index 00000000000..b797c7e7a48 --- /dev/null +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml @@ -0,0 +1,58 @@ +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 = dcount(Id), + FindingTypes = make_set(ActivityType, 10), + Workloads = make_set(K8sWorkload, 10), + Namespaces = make_set(K8sNamespace, 10), + CallerIps = make_set(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 + - entityType: Account + fieldMappings: + - identifier: Name + columnName: Identity +version: 1.0.0 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..a3c6d589160 --- /dev/null +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml @@ -0,0 +1,47 @@ +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 = dcount(Id), + DistinctFindingTypes = dcount(ActivityType), + FindingTypes = make_set(ActivityType, 10), + RemoteIps = make_set(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 + - entityType: Account + fieldMappings: + - identifier: Name + columnName: Actor +version: 1.0.0 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..ec48ec1f497 --- /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 = coalesce( + tostring(ResourceDetails.accessKeyDetails.userName), + tostring(ServiceDetails.action.awsApiCallAction.remoteIpDetails.ipAddressV4) + ), + CallerIp = tostring(ServiceDetails.action.awsApiCallAction.remoteIpDetails.ipAddressV4) + | summarize + FindingCount = dcount(Id), + FindingTypes = make_set(ActivityType, 10), + PublicAccessStates = make_set(PublicAccessEffective, 5), + Actors = make_set(ActorUserName, 10), + CallerIps = make_set(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.0 From e8da8067561a218d38605a821ee2a92bdb1930ab Mon Sep 17 00:00:00 2001 From: OluOlus Date: Tue, 28 Jul 2026 09:19:14 +0100 Subject: [PATCH 2/4] Address automated review feedback - Parser: drop DNS_REQUEST (no remoteIpDetails to normalize), resolve Src/Dst by connectionDirection instead of always treating the AWS resource as Src, treat AWS_API_CALL's caller IP as Src with no fabricated destination, use TimeCreated instead of ingestion time for ASIM event timestamps. - Hunting queries: remove Account entity mappings keyed on fields that mix instance ids, usernames, and the literal "unknown" (high-severity and EKS queries); stop folding the caller IP into the S3 query's actor field now that it has its own CallerIp column. --- .../ASimNetworkSessionAWSGuardDuty.yaml | 99 ++++++++++++------- ...KSPrivilegeEscalationCredentialAccess.yaml | 6 +- ...Duty_HighSeverityFindingsCrossAccount.yaml | 6 +- .../AWS_GuardDuty_S3PublicExposure.yaml | 7 +- 4 files changed, 69 insertions(+), 49 deletions(-) diff --git a/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml b/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml index e946ec6f15e..f7edbd31030 100644 --- a/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml +++ b/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml @@ -1,7 +1,7 @@ Parser: Title: Network Session ASIM parser for AWS GuardDuty findings - Version: '0.1.0' - LastUpdated: Jul 27, 2026 + Version: '0.1.1' + LastUpdated: Jul 28, 2026 Product: Name: AWS GuardDuty Normalization: @@ -29,29 +29,47 @@ ParserQuery: | 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", "DNS_REQUEST") + | where ActionType_ in ("NETWORK_CONNECTION", "AWS_API_CALL", "PORT_PROBE") | extend NetworkAction_ = ServiceDetails.action.networkConnectionAction, ApiCallAction_ = ServiceDetails.action.awsApiCallAction, - PortProbeAction_ = ServiceDetails.action.portProbeAction, - DnsAction_ = ServiceDetails.action.dnsRequestAction - // Prefer the network-connection view; fall back to the API-call caller IP for IAM-driven findings. + PortProbeAction_ = ServiceDetails.action.portProbeAction + // 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", PortProbeAction_.portProbeDetails[0].remoteIpDetails, dynamic(null) ) - | extend DstIpAddr_ = tostring(RemoteDetails_.ipAddressV4) - | where isnotempty(DstIpAddr_) + | 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(PortProbeAction_.portProbeDetails[0].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 = TimeGenerated, - EventEndTime = TimeGenerated, + EventStartTime = TimeCreated, + EventEndTime = TimeCreated, EventType = "NetworkSession", EventSchema = "NetworkSession", EventSchemaVersion = "0.2.6", @@ -75,23 +93,33 @@ ParserQuery: | EventOriginalType = ActivityType, EventOriginalUid = Id, EventUid = strcat("guardduty-", Id) - // Source (the AWS resource the finding is about) + // 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 = coalesce(tostring(NetIf0_.privateIpAddress), tostring(NetIf0_.publicIp), ""), - SrcPortNumber = toint(NetworkAction_.localPortDetails.port), - SrcHostname = tostring(InstanceDetails_.instanceId), - SrcDvcId = tostring(InstanceDetails_.instanceId), - SrcDvcIdType = iff(isnotempty(tostring(InstanceDetails_.instanceId)), "Other", ""), - SrcDomain = tostring(NetIf0_.vpcId), - SrcDomainType = iff(isnotempty(tostring(NetIf0_.vpcId)), "Other", "") - // Destination (the remote endpoint GuardDuty observed) + 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 - DstIpAddr = DstIpAddr_, - DstPortNumber = toint(coalesce(NetworkAction_.remotePortDetails.port, dynamic(null))), - DstGeoCountry = tostring(RemoteDetails_.country.countryName), - DstGeoCity = tostring(RemoteDetails_.city.cityName), - DstDescription = tostring(RemoteDetails_.organization.org) - // Network fields + 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("")))), NetworkDirection = case( @@ -103,11 +131,11 @@ ParserQuery: | NetworkBytes = int(0), NetworkPackets = int(0), NetworkApplicationProtocol = case( - toint(NetworkAction_.remotePortDetails.port) == 80, "HTTP", - toint(NetworkAction_.remotePortDetails.port) == 443, "HTTPS", - toint(NetworkAction_.remotePortDetails.port) == 53, "DNS", - toint(NetworkAction_.remotePortDetails.port) == 22, "SSH", - toint(NetworkAction_.remotePortDetails.port) == 3389, "RDP", + 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) @@ -122,13 +150,14 @@ ParserQuery: | ), DvcAction = iff(tobool(NetworkAction_.blocked) == true, "Deny", "Allow"), DvcInterface = tostring(NetIf0_.networkInterfaceId) - // Threat fields + // 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 = "DstIpAddr", - ThreatIpAddr = DstIpAddr, + ThreatField = iff(IsApiCall_ or IsInbound_, "SrcIpAddr", "DstIpAddr"), + ThreatIpAddr = RemoteIp_, ThreatIsActive = true, ThreatFirstReportedTime = TimeCreated, ThreatLastReportedTime = TimeGenerated @@ -157,7 +186,9 @@ ParserQuery: | EventSeverity, EventOriginalSeverity, EventMessage, EventOriginalType, EventOriginalUid, EventUid, SrcIpAddr, SrcPortNumber, SrcHostname, SrcDvcId, SrcDvcIdType, SrcDomain, SrcDomainType, - DstIpAddr, DstPortNumber, DstGeoCountry, DstGeoCity, DstDescription, + 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, diff --git a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml index b797c7e7a48..50c3bf692ce 100644 --- a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml @@ -51,8 +51,4 @@ entityMappings: fieldMappings: - identifier: AppId columnName: AccountId - - entityType: Account - fieldMappings: - - identifier: Name - columnName: Identity -version: 1.0.0 +version: 1.0.1 diff --git a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml index a3c6d589160..bb797e564c7 100644 --- a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml @@ -40,8 +40,4 @@ entityMappings: fieldMappings: - identifier: AppId columnName: AccountId - - entityType: Account - fieldMappings: - - identifier: Name - columnName: Actor -version: 1.0.0 +version: 1.0.1 diff --git a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml index ec48ec1f497..bf35ae50caf 100644 --- a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml @@ -23,10 +23,7 @@ query: | BucketName = tostring(ResourceDetails.s3BucketDetails[0].name), BucketArn = tostring(ResourceDetails.s3BucketDetails[0].arn), PublicAccessEffective = tostring(ResourceDetails.s3BucketDetails[0].publicAccess.effectivePermission), - ActorUserName = coalesce( - tostring(ResourceDetails.accessKeyDetails.userName), - tostring(ServiceDetails.action.awsApiCallAction.remoteIpDetails.ipAddressV4) - ), + ActorUserName = tostring(ResourceDetails.accessKeyDetails.userName), CallerIp = tostring(ServiceDetails.action.awsApiCallAction.remoteIpDetails.ipAddressV4) | summarize FindingCount = dcount(Id), @@ -44,4 +41,4 @@ entityMappings: fieldMappings: - identifier: AppId columnName: AccountId -version: 1.0.0 +version: 1.0.1 From fdd133818f5d9aa7b1edb0e3f8300e594233b51b Mon Sep 17 00:00:00 2001 From: OluOlus Date: Tue, 28 Jul 2026 13:33:54 +0100 Subject: [PATCH 3/4] Address second round of automated review feedback Parser: - Expand PORT_PROBE's portProbeDetails array instead of hard-coding index [0], so multi-port probes normalize to one row per probe. - Derive NetworkDirection from the same Src/Dst direction logic used elsewhere, not raw connectionDirection (which is null for AWS_API_CALL/PORT_PROBE and was always falling through to "Unknown"). - NetworkBytes/NetworkPackets are now typed nulls, not 0 - GuardDuty doesn't report counts, so 0 would misrepresent an unknown as measured. - DvcAction is now only set for NETWORK_CONNECTION; AWS_API_CALL and PORT_PROBE no longer default to a fabricated "Allow". Hunting queries: - FindingCount now uses count() instead of dcount(Id); Id is already unique per finding row. - S3 query drops rows with no bucket ARN before summarizing, instead of silently collapsing them into one group with an empty BucketName/Arn. - Set aggregations (RemoteIps, Workloads, Namespaces, CallerIps, PublicAccessStates, Actors) use make_set_if(..., isnotempty(...)) instead of make_set, so empty strings don't pollute the output. --- .../ASimNetworkSessionAWSGuardDuty.yaml | 29 ++++++++++++++----- ...KSPrivilegeEscalationCredentialAccess.yaml | 10 +++---- ...Duty_HighSeverityFindingsCrossAccount.yaml | 6 ++-- .../AWS_GuardDuty_S3PublicExposure.yaml | 13 +++++---- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml b/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml index f7edbd31030..7d67d748076 100644 --- a/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml +++ b/Parsers/ASimNetworkSession/Parsers/ASimNetworkSessionAWSGuardDuty.yaml @@ -1,6 +1,6 @@ Parser: Title: Network Session ASIM parser for AWS GuardDuty findings - Version: '0.1.1' + Version: '0.1.2' LastUpdated: Jul 28, 2026 Product: Name: AWS GuardDuty @@ -37,11 +37,15 @@ ParserQuery: | 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", PortProbeAction_.portProbeDetails[0].remoteIpDetails, + ActionType_ == "PORT_PROBE", PortProbeDetail_.remoteIpDetails, dynamic(null) ) | extend RemoteIp_ = tostring(RemoteDetails_.ipAddressV4) @@ -56,7 +60,7 @@ ParserQuery: | LocalIp_ = coalesce(tostring(NetIf0_.privateIpAddress), tostring(NetIf0_.publicIp), ""), LocalPort_ = case( ActionType_ == "NETWORK_CONNECTION", toint(NetworkAction_.localPortDetails.port), - ActionType_ == "PORT_PROBE", toint(PortProbeAction_.portProbeDetails[0].localPortDetails.port), + ActionType_ == "PORT_PROBE", toint(PortProbeDetail_.localPortDetails.port), int(null) ), RemotePort_ = case( @@ -122,14 +126,18 @@ ParserQuery: | // 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( - tostring(NetworkAction_.connectionDirection) == "INBOUND", "Inbound", - tostring(NetworkAction_.connectionDirection) == "OUTBOUND", "Outbound", + IsApiCall_, "Unknown", + IsInbound_, "Inbound", + ActionType_ == "NETWORK_CONNECTION" and tostring(NetworkAction_.connectionDirection) == "OUTBOUND", "Outbound", "Unknown" ), NetworkSessionId = Id, - NetworkBytes = int(0), - NetworkPackets = int(0), + // 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", @@ -148,7 +156,12 @@ ParserQuery: | isnotempty(tostring(InstanceDetails_.platform)), tostring(InstanceDetails_.platform), "" ), - DvcAction = iff(tobool(NetworkAction_.blocked) == true, "Deny", "Allow"), + // 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. diff --git a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml index 50c3bf692ce..2550b973564 100644 --- a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_EKSPrivilegeEscalationCredentialAccess.yaml @@ -36,11 +36,11 @@ query: | ) | extend Identity = coalesce(K8sUserName, InstanceId, "unknown") | summarize - FindingCount = dcount(Id), + FindingCount = count(), FindingTypes = make_set(ActivityType, 10), - Workloads = make_set(K8sWorkload, 10), - Namespaces = make_set(K8sNamespace, 10), - CallerIps = make_set(CallerIp, 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) @@ -51,4 +51,4 @@ entityMappings: fieldMappings: - identifier: AppId columnName: AccountId -version: 1.0.1 +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 index bb797e564c7..471f8520a93 100644 --- a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_HighSeverityFindingsCrossAccount.yaml @@ -26,10 +26,10 @@ query: | ) | extend Actor = coalesce(AccessKeyUserName, InstanceId, "unknown") | summarize - FindingCount = dcount(Id), + FindingCount = count(), DistinctFindingTypes = dcount(ActivityType), FindingTypes = make_set(ActivityType, 10), - RemoteIps = make_set(RemoteIp, 10), + RemoteIps = make_set_if(RemoteIp, isnotempty(RemoteIp), 10), FirstSeen = min(TimeCreated), LastSeen = max(TimeGenerated) by AccountId, Region, ThreatPurpose, ResourceType, Actor @@ -40,4 +40,4 @@ entityMappings: fieldMappings: - identifier: AppId columnName: AccountId -version: 1.0.1 +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 index bf35ae50caf..57050d53bc5 100644 --- a/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml +++ b/Solutions/Amazon Web Services/Hunting Queries/AWS_GuardDuty_S3PublicExposure.yaml @@ -25,12 +25,15 @@ query: | 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 = dcount(Id), + FindingCount = count(), FindingTypes = make_set(ActivityType, 10), - PublicAccessStates = make_set(PublicAccessEffective, 5), - Actors = make_set(ActorUserName, 10), - CallerIps = make_set(CallerIp, 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) @@ -41,4 +44,4 @@ entityMappings: fieldMappings: - identifier: AppId columnName: AccountId -version: 1.0.1 +version: 1.0.2 From 1a13beb38bc82e76b5c86c53a65dbfde83ad205b Mon Sep 17 00:00:00 2001 From: OluOlus Date: Wed, 29 Jul 2026 08:18:54 +0100 Subject: [PATCH 4/4] Register new GuardDuty hunting queries in the AWS solution manifest Adds the 3 new hunting queries to Data/Solution_AmazonWebServices.json and bumps the solution version to 3.0.11 with a ReleaseNotes.md entry, per maintainer guidance that content added to an existing Solution needs to be registered in its manifest. Does not regenerate Package/mainTemplate.json or produce a new package zip - left to the packaging pipeline/maintainer given the blast radius of repackaging the whole existing solution. --- .../Data/Solution_AmazonWebServices.json | 7 +++++-- Solutions/Amazon Web Services/ReleaseNotes.md | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) 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/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** |