ZIZHUOffice365ManagementAPI.psm1

<#
 .Synopsis
  PowerShell SDK for Office365ManageAPI
 
 .Description
  IT Admin can use this PowerShell module to call Office365ManagementAPI. It suppports all operations of Office365ManagementAPI. Also supports Webhook subscriptions and notifications.
 
 .Example
   # Installl and import this PowerShell Module
   Set-location 'C:\Program Files\WindowsPowerShell\Modules';
   MD ZIZHUOffice365ManagementAPI
   Copy 2 files(ZIZHUOffice365ManagementAPI.psd1 and ZIZHUOffice365ManagementAPI.psm1) to this folder ZIZHUOffice365ManagementAPI
   Import-Module ZIZHUOffice365ManagementAPI
   Import-Module 'C:\Users\doqi.FAREAST\OneDrive - Microsoft\CodeSamples\Code\ZIZHUOffice365ManagementAPI\ZIZHUOffice365ManagementAPI.psm1'
 
 .Example
   # Connect to ZIZHUOffice365ManagementAPI module via client secret
$clientID = 'bc4db1db-b705-434a-91ff-145aa94185c8';
$tenantId = 'cff343b2-f0ff-416a-802b-28595997daa2';
$clientSecret = '';
Connect-Office365ManagementAPI -tenantID $tenantId -clientID $clientID -ClientSecret $clientSecret;
    
   # Connect to ZIZHUOffice365ManagementAPI module via client certificate
$clientID = 'bc4db1db-b705-434a-91ff-145aa94185c8';
$tenantId = 'cff343b2-f0ff-416a-802b-28595997daa2';
$thumbprint = '15958E05E3E4C2E563CE9BC346B25A2D70867048';
$clientcertificate= get-item "cert:\localmachine\my\$thumbprint";
Connect-Office365ManagementAPI -tenantID $tenantId -clientID $clientID -clientcertificate $clientcertificate;
 
   # Connect to ZIZHUOffice365ManagementAPI module via user sign-in
$clientID = '9b0547c4-28b1-466d-a80e-677c6dc42d42';
$tenantId = 'cff343b2-f0ff-416a-802b-28595997daa2';
$redirectUri='https://login.microsoftonline.com/common/oauth2/nativeclient'
$loginHint = 'freeman@vjqg8.onmicrosoft.com';
Connect-Office365ManagementAPI -tenantID $tenantId -clientID $clientID -loginHint $loginHint -redirectUri $redirectUri;
 
   # List available content and receive audit data
$startTime = "2024-03-24T00:00:00";
$endTime = "2024-03-25T00:00:00";
$blobs = Get-AvailableContent -startTime $startTime -endTime $endTime;
Receive-Content -blobs $blobs;
 
   # Get current subscriptions/Stop subscriptions
Get-CurrentSubscriptions;
Stop-Subscription -contentType AuditSharePoint;
Stop-Subscriptions;
 
   # Start thesubscriptions. If don't pass $webHookBody, no webhook for the subscription
$webhookEndpoint='https://3b34-2404-f801-9000-18-b055-6c1c-9de7-1729.ngrok-free.app/api/O365ManagementAPIHttpFunction';
$authId = 'ZIZHUOffice365ManagementAPINotification20240220';
$expiration= "2024-04-14T00:00:00";
$webHookBody=
@"
{
    "webhook" : {
        "address": "$($webhookEndpoint)",
        "authId": "$($authId)",
        "expiration": "$($expiration)"
    }
}
"@;
Start-Subscription AuditAzureActiveDirectory $webHookBody;
Start-Subscription AuditExchange $webHookBody;
Start-Subscription AuditSharePoint $webHookBody;
Start-Subscription AuditGeneral $webHookBody;
Start-Subscription DLPAll $webHookBody;
 
  # List the notifications
$startTime = "2024-03-27T00:00:00";
$endTime = "2024-03-28T00:00:00";
Get-Notifications -startTime $startTime -endTime $endTime -contentType AuditExchange;
 
  # Receive the FriendlyNames for DLP Resource
Receive-ResourceFriendlyNames;
 
  # Clean after usgae
Disconnect-Office365ManagementAPI;
Get-Module ZIZHUOffice365ManagementAPI | Remove-Module;
#>


enum Office365SubscriptionPlanType {
    Enterpriseplan    
    GCCGovernmentPlan
    GCCHighGovernmentPlan
    DoDGovernmentPlan
    GallatinPlan
}
enum ContentType {
    AuditAzureActiveDirectory    
    AuditExchange
    AuditSharePoint
    AuditGeneral
    DLPAll
}
class Blob {
    [string]$contentUri
    [string]$contentId
    [string]$contentType
    [datetime]$contentCreated
    [datetime]$contentExpiration
    [System.Object[]]$auditRecords
}
class WebHook {
    [string]$authId
    [string]$address
    [string]$expiration
    [string]$status    
}
class Subscription {
    [string]$contentType
    [string]$status
    [WebHook]$webhook     
}
class Notification {
    [string]$contentType
    [string]$contentId
    [string]$contentUri
    [string]$notificationStatus
    [datetime]$contentCreated
    [datetime]$notificationSent    
    [datetime]$contentExpiration    
}
[string]$script:tenantID = $null;
[string]$script:clientId = $null;
[string]$script:clientsecret = $null;
[string]$script:redirectUri = $null;
[string]$script:loginHint = $null;
[X509Certificate]$script:clientcertificate = $null;
$script:AuthResult = $null;
[string]$script:root = $null;
[string]$script:scope = $null; 
$script:httpErrorResponse = $null;
$script:maxretries = 4;
$script:sleepSeconds = 2;
function Show-VerboseMessage {
    param(
        [Parameter(Mandatory = $true)][string]$message
    )    
    Write-Verbose "[$((Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss"))]: $message";
    return;
}
function Show-InformationalMessage {
    param(
        [Parameter(Mandatory = $true)][string]$message,
        [Parameter(Mandatory = $false)][System.ConsoleColor]$consoleColor = [System.ConsoleColor]::Gray
    )
    $defaultConsoleColor = $host.UI.RawUI.ForegroundColor;
    $host.UI.RawUI.ForegroundColor = $consoleColor;
    Write-Information -InformationAction Continue -MessageData "[$((Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss"))]: $message";
    $host.UI.RawUI.ForegroundColor = $defaultConsoleColor;
    return;
}
function Show-HttpErrorResponse {
    $script:httpErrorResponse | Format-List;
}
function Show-LastErrorDetails {
    param(
        $lastError = $Error[0]
    )
    $lastError | Format-List -Property * -Force;
    $lastError.InvocationInfo | Format-List -Property *;
    $exception = $lastError.Exception;
    for ($depth = 0; $null -ne $exception; $depth++) {
        Show-InformationalMessage -message "$depth" * 80 -consoleColor Green;
        $exception | Format-List -Property * -Force;               
        $exception = $exception.InnerException;                
    }
}
function Set-RootString {
    param(
        [Parameter(Mandatory = $true)][Office365SubscriptionPlanType]$office365SubscriptionPlanType
    )
    switch ($office365SubscriptionPlanType) {
        Enterpriseplan {
            $script:root = 'https://manage.office.com';
            Break; 
        }
        GCCGovernmentPlan { 
            $script:root = 'https://manage-gcc.office.com';
            Break;
        }
        GCCHighGovernmentPlan {
            $script:root = 'https://manage.office365.us';
            Break;
        }
        DoDGovernmentPlan { 
            $script:root = 'https://manage.protection.apps.mil';
            Break;
        }
        GallatinPlan { 
            $script:root = 'https://manage.office365.cn';            
            Break;
        }
        Default {
            Write-Error "unknown type: $contentTypeData" -ErrorAction Stop;
        }        
    }
    $script:scope = "$script:root/.default";
    Show-VerboseMessage -message "Root of Office365 Management API endpoint: $($script:root) and scope: $($script:scope)";    
    return;
}
function Get-ContentTypeString {
    param(
        [Parameter(Mandatory = $true)][ContentType]$contentTypeData
    )
    [string]$result = $null;
    switch ($contentTypeData) {
        AuditAzureActiveDirectory { $result = "Audit.AzureActiveDirectory"; Break }
        AuditExchange { $result = "Audit.Exchange"; Break }
        AuditSharePoint { $result = "Audit.SharePoint"; Break }
        AuditGeneral { $result = "Audit.General"; Break }
        DLPAll { $result = "DLP.All"; Break }
        Default {
            Write-Error "unknown type: $contentTypeData" -ErrorAction Stop;
        }
    }
    Write-Output $result;
    return;
}
function Get-ContentTypeEnum {
    param(
        [Parameter(Mandatory = $true)][string]$contentTypeString
    )
    switch ($contentTypeString) {
        "Audit.AzureActiveDirectory" { $result = [ContentType]::AuditAzureActiveDirectory; Break }
        "Audit.Exchange" { $result = [ContentType]::AuditExchange; Break }
        "Audit.SharePoint" { $result = [ContentType]::AuditSharePoint; Break }
        "Audit.General" { $result = [ContentType]::AuditGeneral; Break }
        "DLP.All" { $result = [ContentType]::DLPAll; Break }
        Default {
            Write-Error "unknown type: $contentTypeData" -ErrorAction Stop;
        }
    }
    Write-Output $result;
    return;
}
Function Invoke-O365APIHttpRequest {
    param (
        [Parameter(Mandatory = $true)][string]$url,
        [Parameter(Mandatory = $true)][string]$httpVerb,
        [Parameter(Mandatory = $false)][string]$requstBody
    )
    if ($null -eq $script:AuthResult) {
        Write-Error "Not authenticated. Stop." -ErrorAction Stop;
    }    
    Show-VerboseMessage "Invoke-Webrequest $url $httpVerb";
    if ($PSBoundParameters.ContainsKey('requstBody')) {
        Show-VerboseMessage "request body: $requstBody";        
    }
    $httpResponse = $null;
    $script:httpErrorResponse = $null;
    $retryCount = 0;
    do {
        if ($retryCount -gt 1) {
            $sleepSeconds = [math]::Pow($script:sleepSeconds,$retryCount);
            Show-VerboseMessage "Retry Invoke-O365APIHttpRequest $($httpVerb) $($url): $retryCount after $($sleepSeconds) seconds";
            Start-Sleep -Seconds $sleepSeconds;
        }
        Get-OauthToken;
        $headerParams = @{'Authorization' = "$($script:AuthResult.tokentype) $($script:AuthResult.accesstoken)" };
        try {
            if ($PSBoundParameters.ContainsKey("requstBody")) {
                $httpResponse = Invoke-WebRequest -uri $url -Headers $headerParams -Method $httpVerb -ContentType "application/json" -Body $requstBody;
            }
            else {
                $httpResponse = Invoke-WebRequest -uri $url -Headers $headerParams -Method $httpVerb;
            }
        }
        catch {
            Show-InformationalMessage -message "Http error: $($_.Exception.Response) Body: $($_.ErrorDetails.Message)" -consoleColor Red;
            $script:httpErrorResponse = $_.Exception.Response;
        }
        finally {
            $retryCount = $retryCount + 1;
        }
    } until (($null -ne $httpResponse) -or ($retryCount -gt $script:maxretries))

    if (($null -ne $httpResponse) -and ($httpResponse.StatusCode -in (200, 204))) {
        Show-VerboseMessage "HTTP Response: $($httpResponse.RawContent)";        
        Write-Output $httpResponse;
        return;
    }
    Show-HttpErrorResponse;
    Write-Error "API request fails with error. Stop!" -ErrorAction Stop;
}
function Get-OauthToken {
    Show-VerboseMessage "Start to invoke Get-OauthToken";
    $utcNow = (get-date).ToUniversalTime().AddMinutes(1);
    if ($null -ne $script:AuthResult -and ($utcNow -lt $script:AuthResult.ExpiresOn.UtcDateTime)) {
        Show-VerboseMessage "Current accesstoken is valid before $($script:AuthResult.ExpiresOn.UtcDateTime)";
        return;
    }
    if (-not [string]::IsNullOrWhiteSpace($script:redirectUri)) {
        try {
            Show-VerboseMessage "Get-MsalToken via user sign-in";
            $script:AuthResult = Get-MsalToken -ClientId $script:clientId -TenantId $script:tenantID -Silent -LoginHint $script:loginHint -RedirectUri $script:redirectUri -Scopes $script:scope;
        }
        Catch [Microsoft.Identity.Client.MsalUiRequiredException] {
            $script:AuthResult = Get-MsalToken -ClientId $script:clientId -TenantId $script:tenantID -Interactive -LoginHint $script:loginHint -RedirectUri $script:redirectUri -Scopes $script:scope; ;
        }
        Catch {
            Show-LastErrorDetails;
            Write-Error -Message "Can not get the access token, exit." -ErrorAction Stop;
        }
    }
    else {
        try {
            if (-not [string]::IsNullOrWhiteSpace($script:clientsecret)) {
                Show-VerboseMessage "Get-MsalToken via client crendential auth flow";
                $securedclientSecret = ConvertTo-SecureString $script:clientsecret -AsPlainText -Force
                $script:AuthResult = Get-MsalToken -clientID $script:clientId -ClientSecret $securedclientSecret -tenantID $script:tenantID -Scopes $script:scope;
            }
            elseif ($null -ne $script:clientcertificate) {
                $script:AuthResult = Get-MsalToken -clientID $script:clientId -ClientCertificate $script:clientcertificate -tenantID $script:tenantID -Scopes $script:scope;
            }        
        }
        catch {
            Show-LastErrorDetails;
            Write-Error -Message "Can not get the access token, stop." -ErrorAction Stop;
        }
    }
    Show-VerboseMessage "Succeed to invoke Get-OauthToken";
}
function Connect-Office365ManagementAPI {
    param (
        [Parameter(Mandatory = $true)][string]$tenantID,
        [Parameter(Mandatory = $true)][String]$clientId,        
        [Parameter(Mandatory = $true, ParameterSetName = "authorizationcode")][String]$redirectUri,
        [Parameter(Mandatory = $true, ParameterSetName = "authorizationcode")][String]$loginHint,    
        [Parameter(Mandatory = $true, ParameterSetName = "clientcredentialsSecret")][String]$clientsecret,
        [Parameter(Mandatory = $true, ParameterSetName = "clientcredentialsCertificate")][X509Certificate]$clientcertificate,
        [Parameter(Mandatory = $false)][Office365SubscriptionPlanType]$office365SubscriptionPlanType = [Office365SubscriptionPlanType]::Enterpriseplan
    )
    $script:tenantID = $tenantID;
    $script:clientId = $clientId;
    if (-not [string]::IsNullOrWhiteSpace($clientsecret)) {
        $script:clientsecret = $clientsecret;
    }
    elseif ($null -ne $clientcertificate) {
        $script:clientcertificate = $clientcertificate;
    }
    elseif (-not [string]::IsNullOrWhiteSpace($redirectUri)) {
        $script:loginHint = $loginHint;
        $script:redirectUri = $redirectUri;
    }
    else {
        Write-Error "Not implement." -ErrorAction Stop;
    }
    Set-RootString $office365SubscriptionPlanType;
    Get-OauthToken;
    if ($null -eq $script:AuthResult) {
        Write-Error "Can not connect to Office365 Management API. Please check your app registration in AAD." -ErrorAction Stop;
    }
    Show-InformationalMessage -message "Successfuly Connect to Office365 Management API" -consoleColor Green;
}
function Start-Subscription {
    param (
        [Parameter(Mandatory = $true)][ContentType]$contentType,        
        [Parameter(Mandatory = $false)][string]$webhook
    )
    $contentTypestring = Get-ContentTypeString $contentType;
    $Subscriptions = Get-CurrentSubscriptions;
    $subscribedContentType = @($Subscriptions | Where-Object { $PSItem.status -eq "enabled" -and $PSItem.contentType -eq $contentTypeString } );
    if ($subscribedContentType.Count -eq 0) {
        $subscriptionUrl = "$($script:root)/api/v1.0/$($script:tenantID)/activity/feed/subscriptions/start?contentType=$contentTypestring";
        if ($PSBoundParameters.ContainsKey("webhook")) {
            $httpResponse = Invoke-O365APIHttpRequest -url $subscriptionUrl -httpVerb Post -requstBody $webhook;        
        }
        else {
            $httpResponse = Invoke-O365APIHttpRequest -url $subscriptionUrl -httpVerb Post;        
        }
    }
    else {
        Show-InformationalMessage -message "The subscription of $contentType has been started already, and please stop it before start with new parameters" -consoleColor Yellow;
    }
    $httpResponse | Format-List;
}
function Stop-Subscription {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param (
        [Parameter(Mandatory = $true)][ContentType]$contentType
    )
    if ($PSCmdlet.ShouldProcess($contentType)) {
        $contentTypestring = Get-ContentTypeString $contentType;
        $subscriptionUrl = "$($script:root)/api/v1.0/$($script:tenantID)/activity/feed/subscriptions/stop?contentType=$contentTypestring";
        $httpResponse = Invoke-O365APIHttpRequest -url $subscriptionUrl -httpVerb Post;
        $httpResponse | Format-List;
    }
    else {
        Show-InformationalMessage -message "The user decide to not stop subscription $contentType" -consoleColor Yellow;
    }
}
function Stop-Subscriptions {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param()
    if ($PSCmdlet.ShouldProcess('TARGET')) {
        $contentTypes = @(Get-CurrentSubscriptions | Where-Object { $_.status -eq "enabled" } | ForEach-Object { $psitem.contentType });
        $contentTypes | ForEach-Object {
            $contentType = Get-ContentTypeEnum $PSItem;
            Stop-Subscription -contentType $contentType -Confirm:$false;
        }
    }
}
function Get-CurrentSubscriptions {
    $listSubscriptionURI = "$($script:root)/api/v1.0/$($script:tenantID)/activity/feed/subscriptions/list";
    $httpResponse = Invoke-O365APIHttpRequest -url $listSubscriptionURI -httpVerb Get;    
    $convertObjects = $httpResponse.Content | Out-String | ConvertFrom-Json;
    $subscriptions = New-Object Collections.Generic.List[Subscription];
    $convertObjects | ForEach-Object {
        $subscription = New-Object Subscription;
        $subscription.contentType = $psitem.contentType;
        $subscription.status = $psitem.status;
        $subscription.webhook = $psitem.webhook -as [Webhook];
        $subscriptions.Add($subscription);
    }
    Write-Output $subscriptions;
    return;
}
function Get-AvailableContent {
    param (
        [Parameter(Mandatory = $true)][datetime]$startTime,        
        [Parameter(Mandatory = $true)][datetime]$endTime,
        [Parameter(Mandatory = $false)][ContentType]$contentType
    )
    $Subscriptions = Get-CurrentSubscriptions;
    $contentTypes = @($Subscriptions | Where-Object { $_.status -eq "enabled" } | ForEach-Object { $psitem.contentType });
    if ($PSBoundParameters.ContainsKey('contentType')) {
        $contentTypeString = Get-ContentTypeString $contentType;
        $contentTypes = @($contentTypes | Where-Object { $PSItem -eq $contentTypeString });
    }
    if ($contentTypes.Count -eq 0) {
        Write-Warning "The subscription of the sepcify ContentType or any ContentType has been not started yet.";
        return;
    }   
    $availableContent = New-Object Collections.Generic.List[Blob];
    Show-VerboseMessage "Run Get-AvailableContent for the contenttypes $contentTypes";
    $contentTypes | ForEach-Object {
        $enabledContentType = $psitem;
        # List available content
        $contentUrl = "$($script:root)/api/v1.0/$($script:tenantID)/activity/feed/subscriptions/content?contentType=$enabledContentType&startTime=" + $startTime + "&endTime=" + $endTime;        
        While (-not [string]::IsNullOrEmpty($contentUrl)) {
            Show-VerboseMessage "List available content via the Url $contentUrl";
            $httpResponse = Invoke-O365APIHttpRequest -url $contentUrl -httpVerb Get; 
            $convertObjs = ConvertFrom-Json $httpResponse.Content;
            $convertObjs | ForEach-Object {
                $blob = New-Object Blob;
                $blob.contentUri = $psitem.contentUri;
                $blob.contentId = $psitem.contentId;
                $blob.contentType = $psitem.contentType;
                $blob.contentCreated = $psitem.contentCreated;
                $blob.contentExpiration = $psitem.contentExpiration;
                $availableContent.Add($blob);
            }
            if ($null -ne $httpResponse.Headers.'NextPageUri') {
                $nextPageUri = $httpResponse.Headers.'NextPageUri';
                Show-VerboseMessage "NextPageUri is $nextPageUri";
            }
            $contentUrl = $httpResponse.Headers.'NextPageUri';
        }
    }
    Write-Output $availableContent;
    return;
}
function Receive-Content {
    param (
        [Parameter(Mandatory = $true)][System.Object[]]$blobs
    )
    if (($null -eq $blobs) -or ($blobs.Count -eq 0)) {
        Show-InformationalMessage "No available content, exit!" -consoleColor Yellow;
        return;
    }
    $blobs | ForEach-Object {    
        try {
            $blob = $PSItem -as [Blob];
            if ($null -ne $blob) {
                Show-VerboseMessage "Receive content from the content Url $($blob.contentUri)";
                $httpResponse = Invoke-O365APIHttpRequest -url $blob.contentUri -httpVerb Get; 
                $contents = $httpResponse;
                if ($null -ne $contents) {
                    $auditRecords = $contents.Content | Out-String | ConvertFrom-Json;
                    $blob.auditRecords = $auditRecords;
                }
                else {
                    Write-Error "Can not receive content for $($blob)" -ErrorAction Continue;
                }
            }            
        }
        catch {
            Show-LastErrorDetails;
        }        
    }
}
function Receive-Notifications {
    Show-InformationalMessage -message "The content notifications are sent to the webhook. We can not implement this operation in PowerShell Module" -consoleColor Yellow;
}
function Get-Notifications {
    param (
        [Parameter(Mandatory = $true)][datetime]$startTime,        
        [Parameter(Mandatory = $true)][datetime]$endTime,
        [Parameter(Mandatory = $true)][ContentType]$contentType
    )
    $contentTypestring = Get-ContentTypeString $contentType;
    $listNotificationsUrl = "$($script:root)/api/v1.0/$($script:tenantID)/activity/feed/subscriptions/notifications?contentType=$contentTypestring&startTime=" + $startTime + "&endTime=" + $endTime;
    Write-Verbose "List notifications via the Url $listNotificationsUrl";
    $httpResponse = Invoke-O365APIHttpRequest -url $listNotificationsUrl -httpVerb Get;
    $convertObjs = ConvertFrom-Json $httpResponse.Content;
    $notifications = New-Object Collections.Generic.List[Notification];
    $convertObjs | ForEach-Object {
        $notificationObj = New-Object Notification;
        $notificationObj.contentType = $psitem.contentType;
        $notificationObj.contentId = $psitem.contentId;
        $notificationObj.contentUri = $psitem.contentUri;
        $notificationObj.notificationStatus = $psitem.notificationStatus;
        $notificationObj.contentCreated = $psitem.contentCreated;
        $notificationObj.notificationSent = $psitem.notificationSent;
        $notificationObj.contentExpiration = $psitem.contentExpiration;
        $notifications.Add($notificationObj);
    }
    Write-Output $notifications;
    return;      
}
function Receive-ResourceFriendlyNames {
    $url = "$($script:root)/api/v1.0/$($script:tenantID)/activity/feed/resources/dlpSensitiveTypes";
    Show-VerboseMessage "Receive resource FriendlyNames via the Url $url";
    $httpResponse = Invoke-O365APIHttpRequest -url $url -httpVerb Get;
    $friendlyNames = $httpResponse.Content | Out-String | ConvertFrom-Json;
    Write-Output $friendlyNames;
    return;
}
function Disconnect-Office365ManagementAPI {
    $script:tenantID = $null;
    $script:clientId = $null;
    $script:clientsecret = $null;
    $script:redirectUri = $null;
    $script:loginHint = $null;
    $script:clientcertificate = $null;
    $script:AuthResult = $null;
    $script:root = $null;
    $script:scope = $null; 
    $script:httpErrorResponse = $null;
    Show-InformationalMessage -message "Successfuly Disconnect to Office365 Management API" -consoleColor Green;
}
Export-ModuleMember Disconnect-Office365ManagementAPI, Receive-ResourceFriendlyNames, Get-Notifications, Receive-Notifications, Receive-Content, Get-AvailableContent, Get-CurrentSubscriptions, Stop-Subscriptions, Stop-Subscription, Start-Subscription, Connect-Office365ManagementAPI;