Anomalous Resource Creation and related Network Activity
Description
'Indicates when an anomalous number of resources are created in Azure via AzureActivity log. Resource creation could indicate malicious or spurious use of your Azure Resource allocation.'
Query · kql
let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let lookback = totimespan((endtime-starttime)*7);
let activity = AzureActivity
| where TimeGenerated >= startofday(ago(lookback))
// We look for any Operation that created and then succeeded where ActivitySubstatusValue has a value so that we can provide context
| where OperationNameValue endswith "write"
| where ActivityStatusValue has "Succeeded"
| make-series dResourceCount=dcount(ResourceId) default=0 on EventSubmissionTimestamp in range(startofday(ago(7d)), now(), 1d) by Caller, Resource, OperationNameValue
| extend (RSquare,Slope,Variance,RVariance,Interception,LineFit)=series_fit_line(dResourceCount)
// Comment slope reference below to see all returns
| where Slope > 0.2
| join kind=leftsemi (
// Last day's activity is anomalous
AzureActivity
| where TimeGenerated between(starttime..endtime)
// We look for any Operation that created and then succeeded where ActivitySubstatusValue has a value so that we can provide context
| where OperationNameValue endswith "write"
| where ActivityStatusValue has "Succeeded"
| make-series dResourceCount=dcount(ResourceId) default=0 on EventSubmissionTimestamp in range(startofday(ago(1d)), now(), 1d) by Caller, Resource, OperationNameValue
| extend (RSquare,Slope,Variance,RVariance,Interception,LineFit)=series_fit_line(dResourceCount)
// Comment slope reference below to see all returns
| where Slope > 0.2
) on Caller, Resource, OperationNameValue
// Expanding the fields that were grouped so we can match on a time window when we join the details later
| mvexpand EventSubmissionTimestamp, dResourceCount
// Making sure the fields are the right type or the join fails
| extend todatetime(EventSubmissionTimestamp), tostring(dResourceCount)
| join kind= inner (
AzureActivity
| where TimeGenerated between(starttime..endtime)
// We look for any Operation that created and then succeeded where ActivitySubstatusValue has a value so that we can provide context
| where OperationNameValue endswith "write"
| where ActivityStatusValue has "Succeeded" and isnotempty(ActivitySubstatusValue)
| summarize by EventSubmissionTimestamp = bin(EventSubmissionTimestamp, 1d), Caller, CallerIpAddress, OperationNameValue, ActivityStatusValue, Resource, ResourceGroup, ResourceId, SubscriptionId
) on EventSubmissionTimestamp, Caller, Resource, OperationNameValue;
let NetworkAnalytics =
union isfuzzy=true
(AzureNetworkAnalytics_CL
| where TimeGenerated between(starttime..endtime)
// Controlling for Schema Version and later parsing - This is Version 2 and Public IPs only
| where (isnotempty(FASchemaVersion_s) and isnotempty(DestPublicIPs_s))
| extend SchemaVersion = FASchemaVersion_s
| extend PublicIPs = tostring(split(DestPublicIPs_s,"|")[0])
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), FirstProcessedTimeUTC = min(FlowStartTime_t), LastProcessedTimeUtc = max(FlowEndTime_t),
Regions = makeset(Region_s), AzureRegions = makeset(AzureRegion_s), VMs = makeset(VM_s), MACAddresses = makeset(MACAddress_s), PublicIPs = makeset(PublicIPs), DestPort = makeset(DestPort_d), SrcIP = makeset(SrcIP_s),
ActivityCount = count() by NSGRule_s, NSGList_s, SubNet = Subnet1_s, FlowDirection_s, Subscription = Subscription1_g, Tags_s, SchemaVersion
//NSGList_s contains the subscription ID, remove that as we already have a field for this and now it will match what we get for SchemaVersion 1
| extend NSG = case(isnotempty(NSGList_s), strcat(split(NSGList_s, "/")[-2],"/",split(NSGList_s, "/")[-1]), "NotAvailable")
// Depending on the SchemaVersion, we will need to provide the NSG_Name for matching against the resource identified in AzureActivity
| extend NSG_Name = tostring(split(NSG, "/")[-1])
),
(
AzureNetworkAnalytics_CL
| where TimeGenerated between(starttime..endtime)
// Controlling for Schema Version and later parsing - This is Version 1
| where isempty(FASchemaVersion_s)
// Controlling for public IPs only
| where isnotempty(PublicFrontendIPs_s) or isnotempty(PublicIPAddresses_s)
| where PublicFrontendIPs_s != "null" or PublicIPAddresses_s != "null"
| extend SchemaVersion = SchemaVersion_s
// The Public IP can be indicated in one of 2 locations, assigning here for easy union results
| extend PublicIPs = case(isnotempty(PublicFrontendIPs_s), PublicFrontendIPs_s,
PublicIPAddresses_s)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), FirstProcessedTimeUTC = min(TimeProcessed_t), LastProcessedTimeUtc = max(TimeProcessed_t),
Regions = makeset(Region_s), AzureRegions = makeset(DiscoveryRegion_s), VMs = makeset(VirtualMachine_s), MACAddresses = makeset(MACAddress_s), PublicIPs = makeset(PublicIPs),
SrcIP = makeset(PrivateIPAddresses_s), Name = makeset(Name_s), DestPort = makeset(DestinationPortRange_s),
ActivityCount = count() by NSG = NSG_s, SubNet = Subnetwork_s, Subscription = Subscription_g, Tags_s, SchemaVersion
// Some events don't have an NSG listed, populating so it is clear it is not available in th datatype
| extend NSG = case(isnotempty(NSG), NSG, "NotAvailable")
// Depending on the SchemaVersion, we will need to provide the NSG_Name for matching against the resource identified in AzureActivity
| extend NSG_Name = tostring(split(NSG, "/")[-1])
)
| project StartTimeUtc, EndTimeUtc, FirstProcessedTimeUTC, LastProcessedTimeUtc, PublicIPs, NSG, NSG_Name, SrcIP, DestPort, SubNet, Name, VMs, MACAddresses, ActivityCount, Regions, AzureRegions, Subscription, Tags_s, SchemaVersion
;
activity | join kind= leftouter (NetworkAnalytics
) on $left.Resource == $right.NSG_Name
| extend timestamp = StartTimeUtc, AccountCustomEntity = Caller, IPCustomEntity = CallerIpAddress