Query Details

Azure Dev Ops Activity From Newor Rare IP Outside Business Hours

Query

# *Azure DevOps Activity from New/Rare IP Outside Business Hours*

## Query Information

#### MITRE ATT&CK Technique(s)

| Technique ID | Title    | Link    |
| ---  | --- | --- |
| T1078.004 | Cloud Accounts | https://attack.mitre.org/techniques/T1078/004/ |

#### Description

This rule detects Azure DevOps activities originating from new or rarely seen IP addresses outside of defined business hours. It establishes a baseline of historical IP addresses and flags activities from IPs that have been seen for fewer than a specified number of days, indicating potentially suspicious access.

#### Author <Optional>
- **Name: Benjamin Zulliger**
- **Github: https://github.com/benscha/KQLAdvancedHunting**
- **LinkedIn: https://www.linkedin.com/in/benjamin-zulliger/**


## Defender XDR
```KQL
// List non-internal Azure DevOps activities excluding business hours
// Exclude business hours: Mon-Fri 06:30-18:30 and Monday 20:30-23:00
// Configuration
// Define your IP Range
let internalIpRange = "1.2.3.4/16";
// Define your default Business hours 
let businessHoursStart = 6 * 60 + 30;  // 06:30 in minutes
let businessHoursEnd = 18 * 60 + 30;   // 18:30 in minutes
let mondayExtraStart = 20 * 60 + 30;   // 20:30 in minutes
let mondayExtraEnd = 23 * 60;          // 23:00 in minutes
let businessDaysStart = 1;             // Monday
let businessDaysEnd = 5;               // Friday
let monday = 1;
let lookbackPeriod = 1d;
let historicalLookback = 29d;
let minHistoricalDays = 3;             // Minimum number of days for known IPs
// Build historical IP list with day count
let HistoricalIPDays = ADOAuditLogs_CL
| where TimeGenerated >= ago(historicalLookback)
| extend isInternalIp = ipv4_is_in_range(IpAddress, internalIpRange)
| where isInternalIp == false and isnotnull(IpAddress)
| summarize UniqueDays = dcount(format_datetime(TimeGenerated, 'yyyy-MM-dd')) by ActorUPN, IpAddress;
// Current activities outside of business hours
let CurrentActivities = ADOAuditLogs_CL
| where TimeGenerated > ago(lookbackPeriod)
| extend Dow = toint(dayofweek(TimeGenerated) / 1d)
| extend MinutesSinceMidnight = 60 * datetime_part("Hour", TimeGenerated) + datetime_part("Minute", TimeGenerated)
| where not(
    // Mon-Fri 06:30-18:30
    (Dow >= businessDaysStart and Dow <= businessDaysEnd and MinutesSinceMidnight >= businessHoursStart and MinutesSinceMidnight <= businessHoursEnd)
    or
    // Monday 20:30-23:00
    (Dow == monday and MinutesSinceMidnight >= mondayExtraStart and MinutesSinceMidnight <= mondayExtraEnd)
)
| extend isInternalIp = ipv4_is_in_range(IpAddress, internalIpRange)
| where isInternalIp == false and isnotnull(IpAddress)
| summarize CountEvents=count(), FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Actions=make_set(ActionId, 20) by ActorUPN, ActorDisplayName, IpAddress, UserAgent;
// Join with historical data and filter for new/rare IPs
CurrentActivities
| join kind=leftouter HistoricalIPDays on ActorUPN, IpAddress
| extend 
    IPSeenDays = coalesce(UniqueDays, 0),
    IPRiskLevel = case(
        isempty(UniqueDays) or UniqueDays == 0, "High Risk - New IP",
        UniqueDays < minHistoricalDays, "Medium Risk - Rare IP",
        UniqueDays >= minHistoricalDays, "Lower Risk - Known IP",
        "Unknown"
    )
// Only alert for new or rare IPs (less than 3 days)
| where IPRiskLevel in ("High Risk - New IP", "Medium Risk - Rare IP")

```

Explanation

This query is designed to detect potentially suspicious activities in Azure DevOps by identifying actions from new or rarely seen IP addresses outside of normal business hours. Here's a simplified breakdown of what the query does:

  1. Configuration:

    • It defines what constitutes an internal IP address and sets the business hours as Monday to Friday from 06:30 to 18:30, with an additional window on Monday from 20:30 to 23:00. - It specifies a lookback period of 1 day for current activities and 29 days for historical IP data.
    • It sets a threshold of 3 days for an IP to be considered known.
  2. Historical Data:

    • The query first gathers historical data on IP addresses used in Azure DevOps activities over the past 29 days.
    • It counts the number of unique days each IP address has been seen, excluding internal IPs.
  3. Current Activities:

    • It then looks at current Azure DevOps activities within the last day.
    • It filters out activities that occur during the defined business hours and excludes internal IPs.
    • It summarizes these activities by user and IP address, noting the number of events and the time range they occurred.
  4. Risk Assessment:

    • The query joins current activities with historical data to assess the risk level of each IP address.
    • IPs are categorized as "High Risk" if they are new, "Medium Risk" if they are rare (seen on fewer than 3 days), and "Lower Risk" if they are known.
  5. Alerting:

    • Finally, it filters the results to only include activities from IPs deemed "High Risk" or "Medium Risk," which are considered potentially suspicious.

In summary, this query helps identify unusual Azure DevOps activities by flagging actions from new or infrequently seen IP addresses outside of normal working hours, which could indicate unauthorized access attempts.

Details

Benjamin Zulliger profile picture

Benjamin Zulliger

Released: February 26, 2026

Tables

ADOAuditLogs_CL

Keywords

AzureDevOpsIPAddressActorUserAgentTimeGeneratedActionId

Operators

letagoipv4_is_in_rangeisnotnullsummarizedcountformat_datetimetointdayofweekdatetime_partnotextendwherecountminmaxmake_setjoinkindleftoutercoalescecaseisemptyin

Actions