{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "workspace": {
            "type": "String"
        }
    },
    "resources": [
        {
            "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/external-user-added-and-removed-in-short-timeframe')]",
            "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/external-user-added-and-removed-in-short-timeframe')]",
            "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules",
            "kind": "Scheduled",
            "apiVersion": "2023-12-01-preview",
            "properties": {
                "displayName": "External Guest Added And Removed From Teams With Evidence Of Access",
                "description": "Detects an external guest added to a Microsoft Teams team and removed from that same team within two hours, then scores the pair on what the guest actually did while inside. The add and remove pair is only the trigger; the alert is gated on evidence of access, and an established supplier relationship subtracts from the score.\n\nTABLES: OfficeActivity, SigninLogs, AuditLogs, IdentityInfo\nCONNECTORS: Microsoft 365 (Teams plus SharePoint and OneDrive workloads), Microsoft Entra ID (SigninLogs, AuditLogs), UEBA for IdentityInfo\nLICENSE: Microsoft 365 E3 with unified audit logging. Entra ID P2 adds the actor risk signal; without P2 RiskLevelDuringSignIn is hidden and that signal stays silent.\n\nSCORING (additive, fires at 45):\nGuest downloaded, synced or copied content while inside = 65 (clears the gate through full exculpation)\nGuest opened content but took no copy = 20\nGuest signed in interactively inside the membership window = 15\nInvitation or redemption observed within 14 days = 20\nSame guest added and removed across more than one team = 20\nAdding account ran the same pattern on 2 or more other guests in 7 days = 25\nAdding account had a medium or high risk sign in in 14 days = 25\nEstablished supplier: 3 or more guests already carry the home domain = MINUS 20\n\nTUNING:\n1. Query period must be P14D. The 14d onboarding, partner domain and actor risk lookups return nothing if the rule does not look back that far. The query period is not the detection window.\n2. Frequency must be PT1H and DetectionWindow must match it. The seed is bounded on ingestion_time() within the run interval and on event time across DetectionWindow + IngestionLag + MaxResidency, so a removal fires exactly once whether it arrives on time or hours late.\n3. IngestionLag (4h): set to the real OfficeActivity delay in your tenant. It widens what the seed can see, never what may fire.\n4. GovernanceAccounts: add lower case UPNs of guest lifecycle service accounts before enabling.\n5. MaxResidency (2h): longest add to remove gap that still counts.\n6. PartnerFloor (3): guests sharing a home domain before the supplier relationship counts as established.\n7. AdderLookback (7d): repeat pattern scan, the most expensive lookup in the query.\n8. TeamGuid falls back to TeamName where the tenant does not populate it.\n\nKNOWN FALSE POSITIVES:\n1. Wrong contact chosen in the member picker and removed within minutes. No sign in, no file activity, scores 0 or below, suppressed by the gate rather than an exclusion list.\n2. Access review or entitlement management expiry batches. Add the service account to GovernanceAccounts.\n3. External board member or auditor added for a meeting and removed afterwards. Reading without downloading scores 35 and the established supplier weight takes it to 15.\n4. IT staff testing guest access in a sandbox team. Repeats score 25 alone, below the gate.\n5. Non-interactive token refresh is excluded by design; it continues after removal and proves nothing.\n\nINVESTIGATION STEPS:\n1. Read RiskIndicators first. Downloads plus a recent invitation is the combination to act on.\n2. Check TopFiles and the site count for what left the team, and confirm the sensitivity of the site.\n3. Check GuestSignInIP, GuestSignInGeo and GuestMFA against the expected posture for that partner.\n4. Look up who invited GuestHomeUPN in AuditLogs. The invitation is where a compromise starts; the guest is only the tooling.\n5. Check AddedBy against AddedByDept, AddedByTitle, AddedByManager, AddedByRoles and AddedByEnabled. Ask whether this person has any business adding external members to this team.\n6. If AddedByRisk is populated, treat the adding account as suspect and review its sign ins directly.\n7. If PriorPairs is above zero, pull the other guests the same account cycled through in the last 7 days.\n\nBLIND SPOTS:\nShared channels and B2B direct connect members do not carry #EXT# in the same shape and are out of scope. A guest left in place is not covered; that axis belongs to the guest invitation rule. Teams chat exfiltration leaves no SharePoint telemetry, so read-and-photograph scores 35 and stays silent. The 14 day window means absence proves nothing: a guest invited earlier than that is not aged, and the signal stays quiet rather than guessing. The repeat check only counts guests the same account both added and removed. If the add arrives after the removal, the pair is lost.\n\nAuthor: Bartosz Wysocki | https://www.itprofessor.cloud\nVersion: 1.0 | 2026-08-12",
                "severity": "Medium",
                "enabled": true,
                "query": "// =====================================================================\n// External Guest Added And Removed From Teams With Evidence Of Access\n// =====================================================================\n// Tables  : OfficeActivity, SigninLogs, AuditLogs, IdentityInfo\n// License : Microsoft 365 E3 with audit logging; Entra ID P2 adds the actor risk signal\n// Tuning  : see rule description for full tuning notes\n// Author  : Bartosz Wysocki | itprofessor.cloud\n// Version : 1.0 | 2026-08-12\n// =====================================================================\n// ---------------------------------------------------------------------\n// Tuning\n// ---------------------------------------------------------------------\n// Which events may fire: must equal the rule's run frequency\nlet DetectionWindow  = 1h;\n// Real ingestion delay of the seed table; widens sight, not firing\nlet IngestionLag     = 4h;\nlet MaxResidency     = 2h;   // longest add to remove gap that still counts\nlet HistoryLookback  = 14d;  // onboarding, actor risk and partner domain lookups\nlet AdderLookback    = 7d;   // repeat pattern scan, the most expensive lookup\nlet IdentityLookback = 14d;  // IdentityInfo snapshot window\nlet RepeatThreshold  = 2;    // prior guests this actor both added and removed before it is a pattern\nlet PartnerFloor     = 3;    // other guests on the same home domain before the relationship is established\n// Event time reach of the seed, wide enough to pair a removal with its add\nlet SeedEventWindow  = DetectionWindow + IngestionLag + MaxResidency;\n// ---------------------------------------------------------------------\n// Scoring, additive, gated at FireThreshold\n// ---------------------------------------------------------------------\nlet W_GuestDownload      = 65;  // guest took a copy of team content, clears the gate through full exculpation\nlet W_GuestFileAccess    = 20;  // guest opened content but took no copy\nlet W_GuestSignedIn      = 15;  // successful interactive guest sign in inside the membership window\nlet W_GuestRecent        = 20;  // invitation or redemption observed inside the history window\nlet W_MultiTeam          = 20;  // same guest added and removed across more than one team\nlet W_RepeatAdder        = 25;  // this actor has run the same add and remove pattern before\nlet W_AdderRisk          = 25;  // adding account had a medium or high risk sign in, needs Entra ID P2\nlet W_EstablishedPartner = -20; // exculpatory cap: home domain already supplies several guests\nlet FireThreshold        = 45;\n// ---------------------------------------------------------------------\n// Lists\n// ---------------------------------------------------------------------\nlet GovernanceAccounts = dynamic([]);  // guest lifecycle service accounts, lower case\nlet DownloadOps = dynamic([\"FileDownloaded\", \"FileSyncDownloadedFull\", \"FileSyncDownloadedPartial\", \"FileCopied\"]);\nlet ReadOps = dynamic([\"FileAccessed\", \"FileAccessedExtended\", \"FilePreviewed\", \"ListItemViewed\", \"PageViewed\"]);\n// ---------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------\n// chris_northwind.com#EXT#@contoso.onmicrosoft.com becomes chris@northwind.com.\n// contains, never has: #EXT# is not a term, so has \"#EXT#\" is false on every value.\nlet ToHomeUPN = (upn: string) {\n    let stub = tostring(split(upn, \"#EXT#\", 0)[0]);\n    let domain = extract(@\"_([^_]+)$\", 1, stub);\n    tolower(iff(upn contains \"#EXT#\" and isnotempty(domain),\n                strcat(substring(stub, 0, strlen(stub) - strlen(domain) - 1), \"@\", domain),\n                upn))\n};\n// One row per external member per Teams membership event.\nlet ExternalMemberOps = (op: string, eventWindow: timespan) {\n    OfficeActivity\n    | extend IngestedAt = ingestion_time()\n    | where TimeGenerated >= ago(eventWindow)\n    | where OfficeWorkload == \"MicrosoftTeams\"\n    | where Operation == op\n    | where Members contains \"#EXT#\"\n    | mv-apply M = todynamic(Members) on (\n        where tostring(M.UPN) contains \"#EXT#\"\n        | project GuestUPN = tolower(tostring(M.UPN))\n      )\n    | project TimeGenerated, IngestedAt, GuestUPN, TeamName,\n              TeamKey = tolower(iff(isnotempty(TeamGuid), TeamGuid, TeamName)),\n              Actor = tolower(UserId), ActorIP = ClientIP\n};\n// ---------------------------------------------------------------------\n// Seed: removals ingested in this run interval, paired to their own add\n// ---------------------------------------------------------------------\nlet Removals =\n    ExternalMemberOps(\"MemberRemoved\", SeedEventWindow)\n    | where IngestedAt >= ago(DetectionWindow)\n    | project TimeRemoved = TimeGenerated, GuestUPN, TeamKey, TeamName, RemovedBy = Actor;\nlet Additions =\n    ExternalMemberOps(\"MemberAdded\", SeedEventWindow)\n    | project TimeAdded = TimeGenerated, GuestUPN, TeamKey, AddedBy = Actor, AddedByIP = ActorIP;\nlet Pairs =\n    Removals\n    | join kind=inner (Additions) on GuestUPN, TeamKey\n    | where TimeAdded < TimeRemoved and TimeRemoved - TimeAdded <= MaxResidency\n    | summarize arg_max(TimeAdded, TeamName, AddedBy, AddedByIP, RemovedBy) by GuestUPN, TeamKey, TimeRemoved\n    | where not(set_has_element(GovernanceAccounts, AddedBy))\n    | extend ResidencyMins = toint(datetime_diff(\"minute\", TimeRemoved, TimeAdded));\n// One row per guest, so a multi team sweep lands as a single finding.\nlet GuestSet = materialize(\n    Pairs\n    | summarize FirstAdded = min(TimeAdded), LastRemoved = max(TimeRemoved),\n                ResidencyMins = max(ResidencyMins), TeamCount = dcount(TeamKey),\n                TeamsAffected = strcat_array(make_set(TeamName, 10), \", \"),\n                Adders = make_set(AddedBy, 5), Removers = make_set(RemovedBy, 5),\n                AddedByIP = take_any(AddedByIP)\n        by GuestUPN\n    | extend GuestHomeUPN = ToHomeUPN(GuestUPN),\n             AddedBy = tostring(Adders[0]), RemovedBy = tostring(Removers[0]),\n             SameActor = iff(array_length(set_difference(Removers, Adders)) == 0, \"Yes\", \"No\")\n    | extend GuestHomeDomain = tostring(split(GuestHomeUPN, \"@\", 1)[0])\n);\n// ---------------------------------------------------------------------\n// Lookups, all attached to the seed so they decorate rows, never add them\n// ---------------------------------------------------------------------\n// What the guest touched in SharePoint or OneDrive between add and removal.\nlet GuestFileOps =\n    OfficeActivity\n    | where TimeGenerated >= ago(SeedEventWindow)\n    | where OfficeWorkload in (\"SharePoint\", \"OneDrive\")\n    | extend GuestUPN = tolower(UserId)\n    | where GuestUPN in ((GuestSet | project GuestUPN))\n    | project TimeGenerated, GuestUPN, Operation, SourceFileName, Site_Url\n    | join kind=inner (GuestSet | project GuestUPN, FirstAdded, LastRemoved) on GuestUPN\n    | where TimeGenerated between (FirstAdded .. LastRemoved)\n    | summarize Downloads = countif(set_has_element(DownloadOps, Operation)),\n                Reads = countif(set_has_element(ReadOps, Operation)),\n                SitesTouched = dcount(Site_Url),\n                TopFiles = strcat_array(make_set(SourceFileName, 8), \", \")\n        by GuestUPN;\n// Interactive guest sign ins inside the membership window. Non-interactive token\n// refresh is deliberately out of scope: it continues after removal and proves nothing.\nlet GuestSignIns =\n    SigninLogs\n    | where TimeGenerated >= ago(SeedEventWindow)\n    | where ResultType == 0\n    | extend GuestUPN = tolower(UserPrincipalName)\n    | where GuestUPN in ((GuestSet | project GuestUPN))\n    | project TimeGenerated, GuestUPN, IPAddress, AuthenticationRequirement,\n              GeoCity = tostring(LocationDetails.city),\n              GeoCountry = tostring(LocationDetails.countryOrRegion)\n    | join kind=inner (GuestSet | project GuestUPN, FirstAdded, LastRemoved) on GuestUPN\n    | where TimeGenerated between (FirstAdded .. LastRemoved)\n    | summarize SignInCount = count(),\n                arg_max(TimeGenerated, IPAddress, AuthenticationRequirement, GeoCity, GeoCountry) by GuestUPN\n    | project GuestUPN, SignInCount, GuestSignInIP = IPAddress,\n              GuestSignInGeo = strcat(GeoCity, \", \", GeoCountry),\n              GuestMFA = iff(AuthenticationRequirement =~ \"multiFactorAuthentication\", \"Yes\", \"No\");\n// Invitation or redemption observed in the window. Both sides normalise to the home\n// UPN, because the invite carries the real address and the redemption the mangled one.\nlet GuestOnboarding =\n    AuditLogs\n    | where TimeGenerated >= ago(HistoryLookback)\n    | where OperationName in (\"Invite external user\", \"Redeem external user invite\", \"Add user\")\n    | mv-apply TR = todynamic(TargetResources) on (\n        where tostring(TR.type) =~ \"User\" and isnotempty(tostring(TR.userPrincipalName))\n        | project TargetUPN = tolower(tostring(TR.userPrincipalName))\n      )\n    | extend GuestHomeUPN = ToHomeUPN(TargetUPN)\n    | where GuestHomeUPN in ((GuestSet | project GuestHomeUPN))\n    | summarize GuestSeenFrom = min(TimeGenerated) by GuestHomeUPN;\n// Established supplier: other guests already carrying the same home domain.\nlet PartnerDomains =\n    IdentityInfo\n    | where TimeGenerated >= ago(IdentityLookback)\n    | where AccountUPN contains \"#EXT#\"\n    | summarize by AccountUPN = tolower(AccountUPN)\n    | extend GuestHomeDomain = tostring(split(ToHomeUPN(AccountUPN), \"@\", 1)[0])\n    | where GuestHomeDomain in ((GuestSet | project GuestHomeDomain))\n    | summarize PartnerGuests = dcount(AccountUPN) by GuestHomeDomain;\n// Repeat pattern by the adding account. The window stops short of the detection\n// window so the event being scored cannot baseline itself.\nlet AdderHistory =\n    OfficeActivity\n    | where TimeGenerated between (ago(AdderLookback) .. ago(DetectionWindow))\n    | where OfficeWorkload == \"MicrosoftTeams\"\n    | where Operation in (\"MemberAdded\", \"MemberRemoved\")\n    | where Members contains \"#EXT#\"\n    | extend Actor = tolower(UserId)\n    | where Actor in ((GuestSet | project AddedBy))\n    | mv-apply M = todynamic(Members) on (\n        where tostring(M.UPN) contains \"#EXT#\"\n        | project PastGuest = tolower(tostring(M.UPN))\n      )\n    | summarize Adds = countif(Operation == \"MemberAdded\"), Removes = countif(Operation == \"MemberRemoved\")\n        by Actor, PastGuest\n    | where Adds > 0 and Removes > 0\n    | summarize PriorPairs = dcount(PastGuest) by AddedBy = Actor;\n// Standing directory context for the internal actor.\nlet ActorIdentity =\n    IdentityInfo\n    | where TimeGenerated >= ago(IdentityLookback)\n    | extend AddedBy = tolower(AccountUPN)\n    | where AddedBy in ((GuestSet | project AddedBy))\n    | summarize arg_max(TimeGenerated, Department, JobTitle, Manager, AssignedRoles, IsAccountEnabled) by AddedBy\n    | project AddedBy, AddedByDept = Department, AddedByTitle = JobTitle,\n              AddedByManager = tostring(Manager),\n              AddedByRoles = strcat_array(todynamic(AssignedRoles), \", \"),\n              AddedByEnabled = tostring(IsAccountEnabled);\n// Actor risk. Without Entra ID P2 the value is hidden and this stays silent.\nlet ActorRisk =\n    SigninLogs\n    | where TimeGenerated >= ago(HistoryLookback)\n    | where RiskLevelDuringSignIn in (\"high\", \"medium\")\n    | extend AddedBy = tolower(UserPrincipalName)\n    | where AddedBy in ((GuestSet | project AddedBy))\n    | summarize RiskSignIns = count(),\n                AddedByRisk = strcat_array(make_set(RiskLevelDuringSignIn, 2), \", \") by AddedBy;\n// ---------------------------------------------------------------------\n// Score and output\n// ---------------------------------------------------------------------\nGuestSet\n| join kind=leftouter (GuestFileOps) on GuestUPN\n| join kind=leftouter (GuestSignIns) on GuestUPN\n| join kind=leftouter (GuestOnboarding) on GuestHomeUPN\n| join kind=leftouter (PartnerDomains) on GuestHomeDomain\n| join kind=leftouter (AdderHistory) on AddedBy\n| join kind=leftouter (ActorIdentity) on AddedBy\n| join kind=leftouter (ActorRisk) on AddedBy\n| extend Downloads = coalesce(Downloads, 0), Reads = coalesce(Reads, 0),\n         SitesTouched = coalesce(SitesTouched, 0), SignInCount = coalesce(SignInCount, 0),\n         PriorPairs = coalesce(PriorPairs, 0), PartnerGuests = coalesce(PartnerGuests, 0),\n         GuestSeenDays = iff(isnotnull(GuestSeenFrom), toint(datetime_diff(\"day\", now(), GuestSeenFrom)), int(null))\n| extend\n    Risk_1 = iff(Downloads > 0, strcat(\"Guest downloaded or synced \", Downloads, \" items across \", SitesTouched, \" sites while inside the team\"), \"\"),\n    Risk_2 = iff(Downloads == 0 and Reads > 0, strcat(\"Guest opened \", Reads, \" items but took no copy\"), \"\"),\n    Risk_3 = iff(SignInCount > 0, strcat(\"Guest signed in interactively during the membership window from \", GuestSignInGeo), \"\"),\n    Risk_4 = iff(isnotnull(GuestSeenFrom), strcat(\"Guest invitation or redemption observed \", GuestSeenDays, \" days ago\"), \"\"),\n    Risk_5 = iff(TeamCount > 1, strcat(\"Same guest added and removed across \", TeamCount, \" teams\"), \"\"),\n    Risk_6 = iff(PriorPairs >= RepeatThreshold, strcat(AddedBy, \" added and removed \", PriorPairs, \" other external accounts in the last 7 days\"), \"\"),\n    Risk_7 = iff(isnotempty(AddedByRisk), strcat(\"Adding account had \", RiskSignIns, \" sign ins rated \", AddedByRisk), \"\"),\n    Risk_8 = iff(PartnerGuests >= PartnerFloor, strcat(\"Established supplier: \", PartnerGuests, \" guests already carry the domain \", GuestHomeDomain), \"\")\n| extend RiskScore = iff(isnotempty(Risk_1), W_GuestDownload, 0)\n                   + iff(isnotempty(Risk_2), W_GuestFileAccess, 0)\n                   + iff(isnotempty(Risk_3), W_GuestSignedIn, 0)\n                   + iff(isnotempty(Risk_4), W_GuestRecent, 0)\n                   + iff(isnotempty(Risk_5), W_MultiTeam, 0)\n                   + iff(isnotempty(Risk_6), W_RepeatAdder, 0)\n                   + iff(isnotempty(Risk_7), W_AdderRisk, 0)\n                   + iff(isnotempty(Risk_8), W_EstablishedPartner, 0)\n| where RiskScore >= FireThreshold\n| extend RiskIndicators = trim(@\"\\s\\|\\s*$\", strcat(\n      iff(isnotempty(Risk_1), strcat(Risk_1, \" | \"), \"\"),\n      iff(isnotempty(Risk_2), strcat(Risk_2, \" | \"), \"\"),\n      iff(isnotempty(Risk_3), strcat(Risk_3, \" | \"), \"\"),\n      iff(isnotempty(Risk_4), strcat(Risk_4, \" | \"), \"\"),\n      iff(isnotempty(Risk_5), strcat(Risk_5, \" | \"), \"\"),\n      iff(isnotempty(Risk_6), strcat(Risk_6, \" | \"), \"\"),\n      iff(isnotempty(Risk_7), strcat(Risk_7, \" | \"), \"\"),\n      iff(isnotempty(Risk_8), strcat(Risk_8, \" | \"), \"\")\n  ))\n| extend GuestAccountName = tostring(split(GuestUPN, \"@\", 0)[0]),\n         GuestUPNSuffix   = tostring(split(GuestUPN, \"@\", 1)[0]),\n         GuestHomeName    = tostring(split(GuestHomeUPN, \"@\", 0)[0]),\n         AddedByName      = tostring(split(AddedBy, \"@\", 0)[0]),\n         AddedBySuffix    = tostring(split(AddedBy, \"@\", 1)[0])\n| project TimeGenerated = LastRemoved, StartTime = FirstAdded, EndTime = LastRemoved,\n          RiskScore, RiskIndicators, GuestUPN, GuestHomeUPN, GuestHomeDomain, GuestSeenDays,\n          PartnerGuests, TeamsAffected, TeamCount, ResidencyMins, AddedBy, RemovedBy, SameActor,\n          AddedByDept, AddedByTitle, AddedByManager, AddedByRoles, AddedByEnabled, AddedByRisk,\n          PriorPairs, FilesDownloaded = Downloads, FilesAccessed = Reads, TopFiles, SitesTouched,\n          GuestSignInIP, GuestSignInGeo, GuestMFA, AddedByIP,\n          GuestAccountName, GuestUPNSuffix, GuestHomeName, AddedByName, AddedBySuffix\n| sort by RiskScore desc, EndTime desc",
                "queryFrequency": "PT1H",
                "queryPeriod": "P14D",
                "triggerOperator": "GreaterThan",
                "triggerThreshold": 0,
                "suppressionDuration": "PT1H",
                "suppressionEnabled": false,
                "startTimeUtc": null,
                "tactics": [
                    "InitialAccess",
                    "Persistence",
                    "Collection",
                    "DefenseEvasion"
                ],
                "techniques": [
                    "T1078",
                    "T1098",
                    "T1213",
                    "T1070"
                ],
                "subTechniques": [
                    "T1078.004",
                    "T1213.002",
                    "T1070.009"
                ],
                "alertRuleTemplateName": null,
                "incidentConfiguration": {
                    "createIncident": true,
                    "groupingConfiguration": {
                        "enabled": true,
                        "reopenClosedIncident": false,
                        "lookbackDuration": "PT8H",
                        "matchingMethod": "Selected",
                        "groupByEntities": [
                            "Account"
                        ],
                        "groupByAlertDetails": null,
                        "groupByCustomDetails": null
                    }
                },
                "eventGroupingSettings": {
                    "aggregationKind": "AlertPerResult"
                },
                "alertDetailsOverride": {
                    "alertDescriptionFormat": "{{GuestHomeUPN}} held membership of {{TeamsAffected}} before being removed. Indicators: {{RiskIndicators}}"
                },
                "customDetails": {
                    "RiskScore": "RiskScore",
                    "RiskIndicators": "RiskIndicators",
                    "GuestHomeUPN": "GuestHomeUPN",
                    "GuestSeenDays": "GuestSeenDays",
                    "PartnerGuests": "PartnerGuests",
                    "TeamsAffected": "TeamsAffected",
                    "ResidencyMins": "ResidencyMins",
                    "AddedBy": "AddedBy",
                    "RemovedBy": "RemovedBy",
                    "AddedByDept": "AddedByDept",
                    "AddedByTitle": "AddedByTitle",
                    "AddedByManager": "AddedByManager",
                    "AddedByRoles": "AddedByRoles",
                    "AddedByEnabled": "AddedByEnabled",
                    "AddedByRisk": "AddedByRisk",
                    "PriorPairs": "PriorPairs",
                    "FilesDownloaded": "FilesDownloaded",
                    "TopFiles": "TopFiles",
                    "GuestMFA": "GuestMFA"
                },
                "entityMappings": [
                    {
                        "entityType": "Account",
                        "fieldMappings": [
                            {
                                "identifier": "Name",
                                "columnName": "GuestAccountName"
                            },
                            {
                                "identifier": "UPNSuffix",
                                "columnName": "GuestUPNSuffix"
                            }
                        ]
                    },
                    {
                        "entityType": "Account",
                        "fieldMappings": [
                            {
                                "identifier": "Name",
                                "columnName": "GuestHomeName"
                            },
                            {
                                "identifier": "UPNSuffix",
                                "columnName": "GuestHomeDomain"
                            }
                        ]
                    },
                    {
                        "entityType": "Account",
                        "fieldMappings": [
                            {
                                "identifier": "Name",
                                "columnName": "AddedByName"
                            },
                            {
                                "identifier": "UPNSuffix",
                                "columnName": "AddedBySuffix"
                            }
                        ]
                    },
                    {
                        "entityType": "IP",
                        "fieldMappings": [
                            {
                                "identifier": "Address",
                                "columnName": "GuestSignInIP"
                            }
                        ]
                    },
                    {
                        "entityType": "IP",
                        "fieldMappings": [
                            {
                                "identifier": "Address",
                                "columnName": "AddedByIP"
                            }
                        ]
                    }
                ],
                "sentinelEntitiesMappings": null,
                "templateVersion": null
            }
        }
    ]
}
