Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
),
Comment thread
OluOlus marked this conversation as resolved.
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)
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading