Query Details

HUNT 17 M365 Guest Cross Workload Profile 90d

Query

// Hunt    : M365 - Guest User Complete Cross-Workload Activity Profile (90d)
// Purpose : Build a full profile of every guest (#EXT#) user that appeared in
//           OfficeActivity in the past 90 days — their activity across Teams,
//           Exchange, SharePoint, and OneDrive, first/last seen, total ops, and
//           suspicious-behaviour flags (after-hours access, many-site spread,
//           operations involving data exfiltration).
//           Use this to review over-privileged, over-active, or forgotten external guests.
// Tables  : OfficeActivity
// Period  : P90D
// Tactics : Discovery, Collection, Exfiltration
// MITRE   : T1087.004, T1530, T1048
//==========================================================================================

let LookbackDays     = 90d;
let SuspiciousDomains = dynamic([
    "gmail.com", "yahoo.com", "hotmail.com", "outlook.com",
    "protonmail.com", "tutanota.com", "guerrillamail.com"]);

// All guest activity across all workloads
let GuestActivity = OfficeActivity
    | where TimeGenerated > ago(LookbackDays)
    | where UserId has "#EXT#" or UserId has "guest"
    // Normalize the external domain from the UPN  e.g. user_contoso.com#EXT#@tenant.com
    | extend ExternalDomain = extract(@"_([^_#]+)#EXT#", 1, tolower(UserId))
    | extend ExternalDomain = iif(isempty(ExternalDomain), "unknown", ExternalDomain)
    | extend Workload = case(
        RecordType == "MicrosoftTeams",                              "Teams",
        RecordType in ("ExchangeAdmin", "ExchangeItem"),             "Exchange",
        RecordType in ("SharePoint","SharePointFileOperation"),       "SharePoint",
        RecordType in ("OneDrive","OneDriveFileOperation"),           "OneDrive",
        "Other")
    | extend IsAfterHours   = hourofday(TimeGenerated) < 6 or hourofday(TimeGenerated) >= 22
    | extend IsExfilOp      = Operation in (
        "FileDownloaded", "FileAccessed", "FileSyncDownloadedFull",
        "MailItemsAccessed", "Send","AnonymousLinkCreated","SecureLinkCreated");

// Aggregate per guest
GuestActivity
| summarize
    FirstSeen           = min(TimeGenerated),
    LastSeen            = max(TimeGenerated),
    TotalEvents         = count(),
    WorkloadsUsed       = make_set(Workload, 10),
    DistinctWorkloads   = dcount(Workload),
    DistinctSites       = dcount(Site_Url),
    DistinctFiles       = dcount(SourceFileName),
    DistinctOps         = dcount(Operation),
    ExfilOpCount        = countif(IsExfilOp),
    AfterHoursCount     = countif(IsAfterHours),
    TopSites            = make_set(Site_Url, 5),
    TopOps              = make_set(Operation, 10)
    by UserId, ExternalDomain
// Computed risk signals
| extend
    ActiveDays          = datetime_diff("day", LastSeen, FirstSeen),
    IsFreeEmailGuest    = ExternalDomain in (SuspiciousDomains),
    MultiWorkloadGuest  = DistinctWorkloads >= 3,
    HighSiteSpread      = DistinctSites >= 10,
    HighExfilRatio      = ExfilOpCount > 0
        and (todouble(ExfilOpCount) / todouble(TotalEvents)) > 0.5,
    FrequentAfterHours  = AfterHoursCount >= 5
| extend RiskScore = toint(
      iif(IsFreeEmailGuest,    2, 0)
    + iif(MultiWorkloadGuest,  1, 0)
    + iif(HighSiteSpread,      2, 0)
    + iif(HighExfilRatio,      3, 0)
    + iif(FrequentAfterHours,  2, 0))
| project
    UserId,
    ExternalDomain,
    FirstSeen,
    LastSeen,
    ActiveDays,
    TotalEvents,
    DistinctWorkloads,
    WorkloadsUsed,
    DistinctSites,
    DistinctFiles,
    ExfilOpCount,
    AfterHoursCount,
    IsFreeEmailGuest,
    MultiWorkloadGuest,
    HighSiteSpread,
    HighExfilRatio,
    FrequentAfterHours,
    RiskScore,
    TopSites,
    TopOps
| sort by RiskScore desc, TotalEvents desc

Explanation

This query is designed to analyze the activities of guest users (identified by "#EXT#") in Microsoft 365 over the past 90 days. It aims to build a comprehensive profile of each guest user by examining their activities across various Microsoft services like Teams, Exchange, SharePoint, and OneDrive. Here's a simplified breakdown of what the query does:

  1. Data Collection: It gathers all activities from the OfficeActivity table for guest users within the last 90 days.

  2. Domain Identification: It extracts the external domain from the user's ID to identify where the guest user is coming from.

  3. Workload Categorization: It categorizes activities into different workloads (Teams, Exchange, SharePoint, OneDrive) based on the type of activity recorded.

  4. Suspicious Activity Flags:

    • After-Hours Access: Flags activities occurring outside of typical working hours (before 6 AM or after 10 PM).
    • Data Exfiltration Operations: Identifies operations that might involve data being downloaded or accessed in a way that could indicate data exfiltration.
  5. Aggregation: For each guest user, it summarizes:

    • First and last seen dates.
    • Total number of events.
    • Number of distinct workloads, sites, files, and operations.
    • Counts of potentially suspicious activities (e.g., after-hours access, exfiltration operations).
  6. Risk Assessment: It calculates a risk score based on several factors:

    • Whether the guest uses a free email domain (e.g., Gmail, Yahoo).
    • If the guest is active across multiple workloads.
    • If the guest accesses many different sites.
    • If a high proportion of their activities involve data exfiltration.
    • If they frequently access the system after hours.
  7. Output: The query projects relevant information for each guest user, including their risk score, and sorts the results by risk score and total events to prioritize potentially risky users.

This analysis helps identify over-privileged, over-active, or forgotten external guests, allowing for better security management and oversight of guest user activities.

Details

David Alonso profile picture

David Alonso

Released: March 18, 2026

Tables

OfficeActivity

Keywords

GuestUserOfficeActivityTeamsExchangeSharePointOneDriveExternalDomainRiskScore

Operators

letdynamicwherehasextendextracttoloweriifisemptycaseinhourofdaysummarizeminmaxcountmake_setdcountcountifbydatetime_difftodoubletointprojectsort bydesc

Actions