Skip to content

Instantly share code, notes, and snippets.

@puttyq
Last active March 25, 2026 11:49
Show Gist options
  • Select an option

  • Save puttyq/764ba84cd37a689a7ca612df7ef3d502 to your computer and use it in GitHub Desktop.

Select an option

Save puttyq/764ba84cd37a689a7ca612df7ef3d502 to your computer and use it in GitHub Desktop.
PSSchema.ps1 - M365 Management Agent Schema and Import Logic (mim-m365-activity)
# Token acquisition in PSImport.ps1 (client credentials flow)
$tokenBody = @{
grant_type = "client_credentials"
client_id = $settings.ClientId
client_secret = $settings.ClientSecret
scope = "https://graph.microsoft.com/.default"
}
$tokenResponse = Invoke-RestMethod \`
-Uri "https://login.microsoftonline.com/$($settings.TenantId)/oauth2/v2.0/token" \`
-Method Post \`
-Body $tokenBody
$accessToken = $tokenResponse.access_token
# Get email activity for the last 30 days
$uri = "https://graph.microsoft.com/v1.0/reports/getEmailActivityUserDetail(period='D30')"
$response = Invoke-RestMethod -Uri $uri \`
-Headers @{ Authorization = "Bearer $accessToken" } \`
-Method Get
# Response is a CSV — pipe straight into ConvertFrom-Csv
$activityData = $response | ConvertFrom-Csv
# 1. Pull email activity report (returns CSV, convert to objects)
$emailActivity = Invoke-RestMethod \`
-Uri "https://graph.microsoft.com/v1.0/reports/getEmailActivityUserDetail(period='D30')" \`
-Headers @{Authorization="Bearer $accessToken"} | ConvertFrom-Csv
# Build a lookup hashtable keyed on UPN for fast joins
$emailIndex = @{}
foreach ($row in $emailActivity) {
$emailIndex[$row.'User Principal Name'] = $row
}
# 2. Enumerate users with signInActivity (paged)
$usersUri = "https://graph.microsoft.com/v1.0/users?`$select=id,userPrincipalName,displayName,signInActivity&`$top=999"
do {
$page = Invoke-RestMethod -Uri $usersUri -Headers @{Authorization="Bearer $accessToken"}
foreach ($user in $page.value) {
$mail = $emailIndex[$user.userPrincipalName]
# Emit combined object to MIM connector space
$obj = New-Object -Type PSCustomObject
$obj | Add-Member -NotePropertyName "Anchor-UPN|String" -NotePropertyValue $user.userPrincipalName
$obj | Add-Member -NotePropertyName "displayName|String" -NotePropertyValue $user.displayName
$obj | Add-Member -NotePropertyName "lastSignInDateTime|String" -NotePropertyValue $user.signInActivity.lastSignInDateTime
$obj | Add-Member -NotePropertyName "mailLastActivityDate|String" -NotePropertyValue $mail?.'Last Activity Date'
$obj | Add-Member -NotePropertyName "mailSendCount|String" -NotePropertyValue $mail?.'Send Count'
$obj | Add-Member -NotePropertyName "mailReceiveCount|String" -NotePropertyValue $mail?.'Receive Count'
$obj # yield to MIM
}
$usersUri = $page.'@odata.nextLink'
} while ($usersUri)
<#
.SYNOPSIS
PSImport.ps1 - M365 Management Agent Import Script
.DESCRIPTION
A PSMA to import M365 activity (directory and Exchange Online) from M365.
This import script reads Entra user, audit logs, and Exchange Online activity from
Microsoft 365 and exposes it to Microsoft Identity Manager / ECMA Host through the Granfeldt PowerShell Management Agent.
Required Entra ID (Azure AD) permissions (Application Registration):
- AuditLog.Read.All (sign-in activity)
- Directory.Read.All (user enumeration)
- Report.Read.All (getEmailActivityUserDetail report)
- User.Read.All (user enumeration)
OPTIONAL - Required Exchange Online permissions (Application Registration):
- Mail.Read (mailbox activity)
- Exchange Admin or delegated mailbox access (for Get-Mailbox and Get-MailboxStatistics)
.NOTES
Version History
v1.0 - Initial M365 PSImport script with sign-in activity and optional mailbox activity
v1.1 - Added feature flag to enable/disable Exchange mailbox check, and error handling for non-mailbox users
v1.2 - Added getEmailActivityUserDetail report import (with configurable period)
v1.3 - Added structured logging via PowerShell Logging module
.LINK
Author - Almero Steyn - https://www.puttyq.com
Integralis Website - http://www.integralis.co.za
Integralis Support - help@integralis.co.za
Granfeldt PowerShell Management Agent - https://github.com/sorengranfeldt/psma
#>
PARAM
(
$Username,
$Password,
$Credentials,
$AuxUsername,
$AuxPassword,
$AuxCredentials,
$OperationType,
$UsePagedImport,
$PageSize,
$ImportPageNumber,
$ConfigurationParameter,
$Schema
)
BEGIN
{
# Setup structured logging using PowerShell Logging module
Import-Module Logging
# Logging - Configure targets
$LogPath = if ($ConfigurationParameter.LogPath) {
$ConfigurationParameter.LogPath
} else {
".\Debug"
}
Add-LoggingTarget -Name File -Configuration @{
Path = "$LogPath\Import-%{+%Y%m%d}.log"
Append = $true
Encoding = 'ascii'
RotateAfterAmount = 10
RotateAmount = 1
RotateAfterSize = 1048576
}
Add-LoggingTarget -Name Console -Configuration @{
PrintException = $true
ColorMapping = @{
'DEBUG' = 'Blue'
'INFO' = 'Green'
'WARNING' = 'Yellow'
'ERROR' = 'Red'
}
}
Set-LoggingDefaultLevel -Level 'DEBUG'
Set-LoggingDefaultFormat -Format '[%{timestamp:+yyyy/MM/dd HH:mm:ss.fff}] [%{level:-7}] - %{message}'
Write-Log -Level 'INFO' -Message 'M365 Activity Import starting'
# Function to get last sent mail date for a user, with error handling for non-mailbox users and feature flag to enable/disable Exchange check
function Get-LastSentMailDate {
param(
[string]$UserPrincipalName,
[hashtable]$Headers,
[bool]$EnableExchangeCheck
)
$lastSentMailDate = $null
if ($EnableExchangeCheck) {
$MailUri = "https://graph.microsoft.com/v1.0/users/$UserPrincipalName/mailFolders('sentitems')/messages?`$top=1&`$orderby=sentDateTime desc"
try {
$MailResponse = Invoke-RestMethod -Uri $MailUri -Headers $Headers
if ($MailResponse.value -and $MailResponse.value.Count -gt 0) {
$lastSentMailDate = $MailResponse.value[0].sentDateTime
Write-Log -Level 'DEBUG' -Message " Last sent mail date: $lastSentMailDate"
}
else {
$lastSentMailDate = $null
}
}
catch {
$lastSentMailDate = $null
Write-Log -Level 'WARNING' -Message " Mailbox not enabled or error: $_"
}
}
else {
$lastSentMailDate = $null
}
return $lastSentMailDate
}
function isValuePresent($passedValue)
{
if($null -ne $passedValue -and $passedValue -ne '')
{
return $true
}
return $false
}
# Variables for Entra App Registration
$ClientId = $Username
$TenantId = $Password
$ClientSecret = $AuxPassword
# Check Exchange mailbox check feature flag
$EnableExchangeCheck = ($ConfigurationParameter.EnableExchangeCheck -eq "true")
# Check getEmailActivityUserDetail via Graph API
if ($ConfigurationParameter.EmailActivityUserDetail -in @('D180','D90','D30','D7')) {
$EmailActivityUserDetail = $true
$EmailActivityUserDetailDays = $ConfigurationParameter.EmailActivityUserDetail
} else {
$EmailActivityUserDetail = $false
$EmailActivityUserDetailDays = 'D0'
}
# Uncomment to override with local test config (see TestConfig.local.ps1.example)
# . "$PSScriptRoot\TestConfig.local.ps1"
# Authenticate to Microsoft Graph (non-interactive)
$TokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
$Body = @{
client_id = $ClientId
scope = "https://graph.microsoft.com/.default"
client_secret = $ClientSecret
grant_type = "client_credentials"
}
try {
$TokenResponse = Invoke-RestMethod -Method Post -Uri $TokenEndpoint -Body $Body
$AccessToken = $TokenResponse.access_token
Write-Log -Level 'INFO' -Message "Successfully authenticated to Microsoft Graph"
}
catch {
Write-Log -Level 'ERROR' -Message "Failed to authenticate to Microsoft Graph: $_"
throw
}
Write-Log -Level 'INFO' -Message "EnableExchangeCheck: $EnableExchangeCheck"
Write-Log -Level 'INFO' -Message "EmailActivityUserDetail: $EmailActivityUserDetail (period: $EmailActivityUserDetailDays)"
}
PROCESS
{
# Get all users from Entra ID in batches, including signInActivity
$Headers = @{ Authorization = "Bearer $AccessToken" }
$users = [System.Collections.Generic.List[object]]::new()
$batchSize = 500
$filter = "userType eq 'Member'"
$select = "id,userPrincipalName,mail,mailNickname,signInActivity"
$nextLink = "https://graph.microsoft.com/v1.0/users?`$filter=$filter&`$select=$select&`$top=$batchSize"
while ($nextLink) {
Write-Log -Level 'DEBUG' -Message "Fetching batch: $nextLink"
try {
$UsersResponse = Invoke-RestMethod -Uri $nextLink -Headers $Headers
}
catch {
Write-Log -Level 'ERROR' -Message "Failed to fetch users from Microsoft Graph: $nextLink - $_"
throw
}
$UsersResponse.value | ForEach-Object { $users.Add($_) }
if ($UsersResponse.'@odata.nextLink') {
$nextLink = $UsersResponse.'@odata.nextLink'
}
else {
$nextLink = $null
}
}
Write-Log -Level 'INFO' -Message "Fetched $($users.Count) users from Entra ID"
# Get EmailActivityUserDetail report details (if enabled)
if ($EmailActivityUserDetail) {
Write-Log -Level 'INFO' -Message "Fetching EmailActivityUserDetail report (period: $EmailActivityUserDetailDays)"
try {
$EmailActivityUserDetailReportResults = Invoke-RestMethod -Method Get -Uri "https://graph.microsoft.com/v1.0/reports/getEmailActivityUserDetail(period='$EmailActivityUserDetailDays')" -Headers $Headers
$EmailActivityUserDetailData = $EmailActivityUserDetailReportResults | ConvertFrom-csv
$EmailActivityLookup = @{}
foreach ($record in $EmailActivityUserDetailData) {
$EmailActivityLookup[$record."User Principal Name"] = $record
}
Write-Log -Level 'INFO' -Message "EmailActivityUserDetail report loaded - $($EmailActivityUserDetailData.Count) records"
}
catch {
Write-Log -Level 'ERROR' -Message "Failed to fetch EmailActivityUserDetail report: $_"
$EmailActivityUserDetail = $false
}
}
# Loop through users and print sign-in dates, with optional Exchange mailbox check
for ($i = 0; $i -lt $users.Count; $i++) {
$user = $users[$i]
$userPrincipalName = $user.userPrincipalName
Write-Log -Level 'INFO' -Message "Processing user $($i + 1)/$($users.Count): $userPrincipalName"
# signInActivity is now included in $user
$lastInteractiveDate = $null
$lastNonInteractiveDate = $null
if ($user.signInActivity) {
$lastInteractiveDate = $user.signInActivity.lastSignInDateTime
$lastNonInteractiveDate = $user.signInActivity.lastNonInteractiveSignInDateTime
}
if ($user.mail -and $user.mailNickname) {
$lastSentMailDate = Get-LastSentMailDate -UserPrincipalName $userPrincipalName -Headers $Headers -EnableExchangeCheck $EnableExchangeCheck
}
else {
$lastSentMailDate = $null
Write-Log -Level 'DEBUG' -Message " Skipping Exchange mailbox check: no mail or mailNickname."
}
# Calculate age in days for each activity type
$now = Get-Date
$interactiveAgeDays = if ($lastInteractiveDate) { [math]::Ceiling(($now - [datetime]$lastInteractiveDate).TotalDays) } else { $null }
$nonInteractiveAgeDays = if ($lastNonInteractiveDate) { [math]::Ceiling(($now - [datetime]$lastNonInteractiveDate).TotalDays) } else { $null }
$sentMailAgeDays = if ($lastSentMailDate) { [math]::Ceiling(($now - [datetime]$lastSentMailDate).TotalDays) } else { $null }
# Emit import object to PSMA pipeline
$obj = @{}
$obj.Add("objectClass", "user")
if (isValuePresent $userPrincipalName) { $obj.Add("UserPrincipalName", $userPrincipalName.ToString()) }
if (isValuePresent $lastInteractiveDate) { $obj.Add("LastInteractiveSignIn", $lastInteractiveDate.ToString()) }
if (isValuePresent $interactiveAgeDays) { $obj.Add("LastInteractiveAgeDays", $interactiveAgeDays.ToString()) }
if (isValuePresent $lastNonInteractiveDate) { $obj.Add("LastNonInteractiveSignIn", $lastNonInteractiveDate.ToString()) }
if (isValuePresent $nonInteractiveAgeDays) { $obj.Add("LastNonInteractiveAgeDays", $nonInteractiveAgeDays.ToString()) }
if (isValuePresent $lastSentMailDate) { $obj.Add("LastSentMailDate", $lastSentMailDate.ToString()) }
if (isValuePresent $sentMailAgeDays) { $obj.Add("LastSentMailAgeDays", $sentMailAgeDays.ToString()) }
if ($EmailActivityUserDetail) {
$ActivityDetails = $EmailActivityLookup[$userPrincipalName]
if ($ActivityDetails) {
Write-Log -Level 'DEBUG' -Message " Email activity data found for $userPrincipalName"
if (isValuePresent $ActivityDetails."Read Count") { $obj.Add("ReportReadCount", ($ActivityDetails."Read Count").ToString()) }
if (isValuePresent $ActivityDetails."Receive Count") { $obj.Add("ReportReceiveCount", ($ActivityDetails."Receive Count").ToString()) }
if (isValuePresent $ActivityDetails."Send Count") { $obj.Add("ReportSendCount", ($ActivityDetails."Send Count").ToString()) }
if (isValuePresent $ActivityDetails."Last Activity Date") { $obj.Add("ReportLastActivityDate", ($ActivityDetails."Last Activity Date").ToString()) }
if (isValuePresent $ActivityDetails."Report Period") { $obj.Add("ReportPeriod", ($ActivityDetails."Report Period").ToString()) }
}
else {
Write-Log -Level 'WARNING' -Message " No email activity report data found for $userPrincipalName"
}
}
$obj
}
}
END
{
Write-Log -Level 'INFO' -Message "M365 Activity Import complete - processed $($users.Count) users"
Wait-Logging
}
<#
.SYNOPSIS
PSSchema.ps1 - M365 Management Agent Schema Definition
.DESCRIPTION
Schema for Entra and Exchange Online last sign-in activity
Source System:
- Entra ID (User and Audit Logs)
- Exchange Online (Optional)
- Exchange Usage Reports (Optional)
.NOTES
Version History
- v1.0 - Initial schema definition for M365 activity data
.LINK
Author - Almero Steyn - https://www.puttyq.com
Integralis Website - http://www.integralis.co.za
Integralis Support - help@integralis.co.za
Granfeldt PowerShell Management Agent - https://github.com/sorengranfeldt/psma
#>
param
(
$Username,
$Password,
$Credentials,
$AuxUsername,
$AuxPassword,
$AuxCredentials,
$ConfigurationParameter
)
$obj = New-Object -Type PSCustomObject
$obj | Add-Member -Type NoteProperty -Name "Anchor-UserPrincipalName|String" -Value "user@domain.com"
$obj | Add-Member -Type NoteProperty -Name "objectClass|String" -Value "user"
$obj | Add-Member -Type NoteProperty -Name "LastInteractiveSignIn|String" -Value "2023-01-01T12:00:00Z"
$obj | Add-Member -Type NoteProperty -Name "LastInteractiveAgeDays|String" -Value "345"
$obj | Add-Member -Type NoteProperty -Name "LastNonInteractiveSignIn|String" -Value "2023-01-15T12:00:00Z"
$obj | Add-Member -Type NoteProperty -Name "LastNonInteractiveAgeDays|String" -Value "345"
$obj | Add-Member -Type NoteProperty -Name "LastSentMailDate|String" -Value "2023-01-20T12:00:00Z"
$obj | Add-Member -Type NoteProperty -Name "LastSentMailAgeDays|String" -Value "345"
$obj | Add-Member -Type NoteProperty -Name "ReportReadCount|String" -Value "100"
$obj | Add-Member -Type NoteProperty -Name "ReportReceiveCount|String" -Value "100"
$obj | Add-Member -Type NoteProperty -Name "ReportSendCount|String" -Value "100"
$obj | Add-Member -Type NoteProperty -Name "ReportLastActivityDate|String" -Value "2023-01-20T12:00:00Z"
$obj | Add-Member -Type NoteProperty -Name "ReportPeriod|String" -Value "D180"
$obj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment