← Back to SOC feed Coverage →

Anomalous Resource Creation and related Network Activity

kql MEDIUM Azure-Sentinel
T1496
AzureActivity
huntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-06-02T11:00:00Z · Confidence: medium

Hunt Hypothesis

Adversaries may create an anomalous number of Azure resources to establish persistence or exfiltrate data, leveraging T1496 to evade detection. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential compromise and mitigate lateral movement or data theft risks.

KQL Query


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

Analytic Rule Definition

id: ac25d05d-362d-4a8d-b4e7-58c0edd2379c
name: 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.'
description_detailed: |  
  'Indicates when an anomalous number of resources are created successfully in Azure via the AzureActivity log.
  This is then joined with the AzureNetworkAnalytics_CL data to identify any network related activity for the created resource.
  The anomaly detection identifies activities that have occured both since the start of the day 1 day ago and the start of the day 7 days ago.
  The start of the day is considered 12am UTC time.
  Resource creation could indicated malicious or spurious use of your Azure Resource allocation.  Resources can be abused in relation to digital
  currency mining, command and control, exfiltration, distributed attacks and propagation of malware, among others. Verify that this resource creation
  is expected.
  Resources:
  https://docs.microsoft.com/azure/azure-monitor/insights/azure-networking-analytics
  https://docs.microsoft.com/azure/network-watcher/traffic-analytics-schema'
requiredDataConnectors:
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
  - connectorId: AzureNetworkWatcher
    dataTypes:
      - AzureNetworkAnalytics_CL
tactics:
  - Impact
relevantTechniques:
  - T1496
query: |

  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
  

Required Data Sources

Sentinel TableNotes
AzureActivityEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/MultipleDataSources/AzureResourceCreationWithNetworkActivity.yaml