Query Details

HUNT 23 M365 Agent ID App Access Profile 30d

Query

// Hunt    : M365 - Agent / Application ID Anomalous Access Profile (30d)
// Purpose : Profile non-interactive application and agent access to M365 resources by
//           AppId. In OfficeActivity, the AppId field identifies the OAuth client
//           (application or agent) that performed the operation — covering Copilot agents,
//           custom connectors, third-party apps, and OAuth-delegated scripts. This hunt
//           surfaces AppIds with bulk, off-hours, or exfiltration-like patterns, and
//           correlates them with the human users they acted on behalf of.
//           Does NOT surface Service Principal sign-ins (see WorkloadIdentitiesDetections).
// Tables  : OfficeActivity
// Period  : P30D
// Tactics : Collection, Exfiltration, Persistence, PrivilegeEscalation
// MITRE   : T1530, T1048, T1119, T1078.004
// Scope   : AppId-based operations on behalf of users or guests (not direct user sessions)
//==========================================================================================

let LookbackDays      = 30d;
let MinEventsToSurface = 20;     // ignore AppIds with very low activity
let HighVolumeThreshold = 500;   // AppIds over this are likely legitimate automation

// Baseline per-app activity
let AppActivity = OfficeActivity
    | where TimeGenerated > ago(LookbackDays)
    | where isnotempty(AppId) and AppId != "00000000-0000-0000-0000-000000000000"
    // Keep only events where an app acted (not pure direct user browser session)
    | where isnotempty(AppId)
    | extend
        IsGuest       = UserId has "#EXT#",
        IsExfilOp     = Operation in (
            "FileDownloaded", "FileSyncDownloadedFull",
            "FileAccessed", "FileSyncUploadedFull",
            "MailItemsAccessed", "Send",
            "AnonymousLinkCreated", "SecureLinkCreated",
            "SharingInvitationCreated"),
        IsAfterHours  = hourofday(TimeGenerated) < 6 or hourofday(TimeGenerated) >= 22,
        Workload      = case(
            RecordType in ("ExchangeAdmin","ExchangeItem"),            "Exchange",
            RecordType in ("SharePoint","SharePointFileOperation"),     "SharePoint",
            RecordType in ("OneDrive","OneDriveFileOperation"),         "OneDrive",
            RecordType == "MicrosoftTeams",                            "Teams",
            RecordType == "ComplianceDLPEndpoint",                     "EndpointDLP",
            RecordType == "AzureActiveDirectory",                      "AAD",
            "Other");

// Aggregate per AppId
let AppProfile = AppActivity
    | summarize
        TotalEvents       = count(),
        ExfilOpCount      = countif(IsExfilOp),
        AfterHoursCount   = countif(IsAfterHours),
        GuestOpCount      = countif(IsGuest),
        DelegatedUsers    = dcount(UserId),
        GuestUsers        = dcountif(UserId, IsGuest),
        HumanUsers        = dcountif(UserId, not(IsGuest)),
        DistinctWorkloads = dcount(Workload),
        Workloads         = make_set(Workload, 5),
        DistinctSites     = dcount(Site_Url),
        DistinctOps       = dcount(Operation),
        TopOps            = make_set(Operation, 15),
        FirstSeen         = min(TimeGenerated),
        LastSeen          = max(TimeGenerated)
        by AppId
    | where TotalEvents >= MinEventsToSurface;

// Flag anomalous patterns
AppProfile
| extend
    ExfilRatio         = round(todouble(ExfilOpCount)    / todouble(TotalEvents), 2),
    AfterHoursRatio    = round(todouble(AfterHoursCount) / todouble(TotalEvents), 2),
    GuestRatio         = round(todouble(GuestOpCount)    / todouble(TotalEvents), 2),
    ActiveDays         = datetime_diff("day", LastSeen, FirstSeen)
| extend RiskScore = toint(
      iif(ExfilRatio       >= 0.5,              3, iif(ExfilRatio       >= 0.2, 1, 0))
    + iif(AfterHoursRatio  >= 0.5,              2, iif(AfterHoursRatio  >= 0.3, 1, 0))
    + iif(GuestRatio       >= 0.3,              2, 0)
    + iif(DelegatedUsers   >= 50,               2, iif(DelegatedUsers   >= 10, 1, 0))
    + iif(DistinctSites    >= 20,               1, 0)
    + iif(TotalEvents      > HighVolumeThreshold, 1, 0))
| extend AnomalyFlags = strcat_array(pack_array(
    iif(ExfilRatio       >= 0.5,              "HighExfilRatio",         ""),
    iif(AfterHoursRatio  >= 0.5,              "BulkAfterHours",         ""),
    iif(GuestRatio       >= 0.3,              "HighGuestOpRatio",       ""),
    iif(DelegatedUsers   >= 50,               "ManyDelegatedUsers",     ""),
    iif(DistinctSites    >= 20,               "WideAccessSpread",       "")), ",")
| project
    AppId,
    RiskScore,
    TotalEvents,
    ExfilOpCount,
    ExfilRatio,
    AfterHoursCount,
    AfterHoursRatio,
    GuestOpCount,
    GuestRatio,
    DelegatedUsers,
    HumanUsers,
    GuestUsers,
    DistinctWorkloads,
    Workloads,
    DistinctSites,
    DistinctOps,
    ActiveDays,
    FirstSeen,
    LastSeen,
    AnomalyFlags,
    TopOps
| sort by RiskScore desc, ExfilOpCount desc

Explanation

This query is designed to identify unusual access patterns by applications or agents (identified by AppId) accessing Microsoft 365 resources over the past 30 days. It focuses on non-interactive access, meaning actions performed by applications or scripts rather than direct user interactions. Here's a simplified breakdown of what the query does:

  1. Data Collection: It gathers data from the OfficeActivity table for the last 30 days, filtering out entries without an AppId or with a default AppId value. It focuses on actions performed by applications rather than direct user sessions.

  2. Activity Profiling: For each AppId, it calculates various metrics:

    • Total number of events.
    • Number of potential data exfiltration operations (e.g., file downloads, email access).
    • Number of operations performed outside of regular working hours.
    • Number of operations involving guest users.
    • Number of distinct users the app acted on behalf of.
    • Number of different workloads (e.g., Exchange, SharePoint) accessed.
    • Number of distinct sites and operations involved.
    • Time range of the app's activity.
  3. Anomaly Detection: It flags AppIds with unusual patterns, such as:

    • High ratio of exfiltration-like operations.
    • High ratio of after-hours activity.
    • High ratio of guest user operations.
    • Large number of delegated users.
    • Access to many different sites.
  4. Risk Scoring: Each AppId is assigned a risk score based on the detected anomalies. Higher scores indicate more suspicious behavior.

  5. Output: The query outputs a list of AppIds with their associated risk scores and activity metrics, sorted by risk score. This helps identify potentially malicious or misconfigured applications that might be accessing sensitive data inappropriately.

Overall, the query is a security-focused analysis aimed at detecting potential misuse or unauthorized access by applications within an organization's Microsoft 365 environment.

Details

David Alonso profile picture

David Alonso

Released: March 18, 2026

Tables

OfficeActivity

Keywords

OfficeActivityAppIdUserIdOperationRecordTypeSiteUrlTimeGenerated

Operators

letagoisnotemptyhasinhourofdaycasesummarizecountcountifdcountdcountifmake_setminmaxextendroundtodoubledatetime_difftointiifstrcat_arraypack_arrayprojectsort by

Actions