Powershell : Automating Yammer User Reports

If you wish to export Yammer reports, yes its now Viva Engage but that name is awful and its still Yammer from a website point of view, despite the new logo that looks like headless people giving each other a hug......

If you need to export Yammer reports then you need to have privileged roles to complete the action which you do not want to be dishing out to normal users so it ends up being a valid user with access that does this operation, if you have a monthly report this can become tedious, so I have created a script to complete this action for me, it pulls all the reports with Graph API (and the relevant delegated API permission) then sends an email to the intended users.

This is what it looks like when running:


Pre-Flight : Prerequisites 

First you need to install the prerequisites before you run this script, if you need to use a proxy uncomment out the lines at the top of the script:

Script : Install-Requirements.ps1

#Required for Proxy only
#$proxy = 'squid.bear.local:3129'
#[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
#[system.net.webrequest]::defaultwebproxy = new-object system.net.webproxy($proxy)
#[system.net.webrequest]::defaultwebproxy.BypassProxyOnLocal = $true

# First, set up PowerShell Gallery and required modules
function Initialize-PowerShellModules {
    Write-Host "Setting up PowerShell Gallery and required modules..." -ForegroundColor Yellow   
    # Set TLS 1.2
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12   

    # Configure default proxy settings if needed
    Write-Host "Configuring proxy settings..." -ForegroundColor Yellow
    [System.Net.WebRequest]::DefaultWebProxy = [System.Net.WebRequest]::GetSystemWebProxy()
    [System.Net.WebRequest]::DefaultWebProxy.Credentials =
[System.Net.CredentialCache]::DefaultNetworkCredentials

       # Force register PowerShell Gallery
   Write-Host "Forcing PowerShell Gallery registration..." -ForegroundColor Yellow
    try {
        $galleryParams = @{
            Name = 'PSGallery'
            SourceLocation = 'https://www.powershellgallery.com/api/v2'
            PublishLocation = 'https://www.powershellgallery.com/api/v2/package'
            InstallationPolicy = 'Trusted'
        }       

        # Remove any existing PSGallery registration
       Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue | 
            Unregister-PSRepository -ErrorAction SilentlyContinue        

        # Register PSGallery with explicit parameters
        Register-PSRepository @galleryParams

        # Verify registration
        $gallery = Get-PSRepository -Name PSGallery -ErrorAction Stop
        Write-Host "PSGallery successfully registered: $($gallery.SourceLocation)" -ForegroundColor Green
    }
    catch {
        Write-Host "Error registering PSGallery: $_" -ForegroundColor Red    

        # Fallback to Register-PSRepository -Default
        Write-Host "Attempting fallback registration..." -ForegroundColor Yellow
        Register-PSRepository -Default -Verbose
    }

       # Install/Import PackageManagement and PowerShellGet first
 Write-Host "Updating core PowerShell package management..." -ForegroundColor Yellow
    $bootstrapModules = @('PackageManagement', 'PowerShellGet')
    foreach ($module in $bootstrapModules) {
        try {
            if (!(Get-Module -Name $module -ListAvailable)) {
                Write-Host "Installing $module..." -ForegroundColor Yellow
                Install-Module -Name $module -Force -AllowClobber -Scope CurrentUser -Verbose
            }
            Import-Module $module -Force
        }
        catch {
            Write-Host "Warning: Could not update $module. Continuing with existing version." -ForegroundColor Yellow
        }
    }  

    # Install/Import NuGet if needed
   if (!(Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue)) {
        Write-Host "Installing NuGet provider..." -ForegroundColor Yellow
        Install-PackageProvider -Name NuGet -Force -Scope CurrentUser -Confirm:$false
    }   

    # Required modules
   $modules = @(
        @{
            Name = "Microsoft.Graph.Authentication"
            MinimumVersion = "1.19.0"
        },
        @{
            Name = "Microsoft.Graph.Reports"
            MinimumVersion = "1.19.0"
        }
    )
    foreach ($module in $modules) {
      Write-Host "Processing module: $($module.Name)..." -ForegroundColor Yellow
        try {
            # Check if module is installed
            $installedModule = Get-Module -ListAvailable -Name $module.Name | 
                Where-Object { $_.Version -ge $module.MinimumVersion }
             if (-not $installedModule) {
               Write-Host "Installing $($module.Name)..." -ForegroundColor Yellow              

                # Try installation with verbose output
                $installParams = @{
                    Name = $module.Name
                    MinimumVersion = $module.MinimumVersion
                    Force = $true
                    AllowClobber = $true
                    Scope = 'CurrentUser'
                    Verbose = $true
                }

               Install-Module @installParams              

                # Verify installation
                $verifyModule = Get-Module -ListAvailable -Name $module.Name | 
                    Where-Object { $_.Version -ge $module.MinimumVersion }
                if (-not $verifyModule) {
                    throw "Module installation verification failed"
                }
            }
            else {
                Write-Host "$($module.Name) version $($installedModule.Version) is already installed" -ForegroundColor Green
            }           

            # Import the module
           Import-Module -Name $module.Name -MinimumVersion $module.MinimumVersion -Force       

            # Verify import
            if (Get-Module -Name $module.Name) {
                Write-Host "$($module.Name) imported successfully" -ForegroundColor Green
            }
            else {
                throw "Module import verification failed"
            }
        }
        catch {
            Write-Host "Error processing $($module.Name): $_" -ForegroundColor Red
            Write-Host "Exception details: $($_.Exception.Message)" -ForegroundColor Red
            throw
        }
    }
}

# Run the initialization
try 

    Initialize-PowerShellModules
    Write-Host "Module setup completed successfully" -ForegroundColor Green
}
catch {
    Write-Host "Failed to set up modules: $_" -ForegroundColor Red
    Write-Host "Exception details: $($_.Exception.Message)" -ForegroundColor Red   

    # Additional troubleshooting information
    Write-Host "`nTroubleshooting information:" -ForegroundColor Yellow
    Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)" -ForegroundColor Yellow
    Write-Host "OS: $([System.Environment]::OSVersion.VersionString)" -ForegroundColor Yellow
    Write-Host "PSGallery Status:" -ForegroundColor Yellow
    Get-PSRepository | Format-Table -AutoSize   

    # Network connectivity test
    Write-Host "`nTesting network connectivity:" -ForegroundColor Yellow
    Test-NetConnection -ComputerName "www.powershellgallery.com" -Port 443 | 
        Select-Object ComputerName, TcpTestSucceeded, PingSucceeded 
  exit 1
}

This will install the required modules with some testing then we can continue on to the main script that does the Graph API connection and extraction, however first we need to setup an application registration (which is OAuth) to handle the secret and API permissions - this is required for the script to work without getting the error "Forbidden".

Pre-Flight : Application Registration

This section will guide you though the application registration which is required for the script, so first visit the Azure Portal website here

Then when here you will need to navigate to Entra ID as below:


Then from here you need App Registrations as below:


Then we need New Registration:


Then give it a name and ensure its is only for a single tenant then click Register:


Then from manage we need to choose Certificates and Secrets:



Then ensure you have client secrets selected and choose New client secret:

Give it a name and a lifetime (I like 12 months) then click the add:


You will then see the client secret and value as below:

Note : You will only see the value once when you navigate away from this screen and back the value will no longer be visible!


Note : Make a note of the value here, this will not require the secret ID

If you need an example of what refreshing the pages does see this example below:


Now we need to go to Manage then API Permissions:



Then you need to add an API permission and choose Microsoft Graph as below:


Then you need application permissions as below:


We now need to add these permissions:


So first search for Report.Read.All in the search box, but a tick in the box and then choose Add Permissions:



The next permission is a Delegated permission, so from Graph API you need the other option as below:


Then you cannot search for User.Read lots of permissions use this, so find the User section choose it from that section as below, tick the box then Add Permission:



You should now see your permissions in API permissions as below, however notice that "Admin Consent" has not been granted (red box) so we need to fix this for the correct API permissions, so click the Grant button (green box):


Then we will be asked to confirm the action and here we need to say yes:


You can confirm this was worked when you see the two green ticks next to the permissions:


That concludes the setup of the application and the API permissions, secrets and delegation, however from this we also need to have the following details handy:

Tenant ID
Application ID
Secret Secret (not the value but the secret)

I have covered the secret earlier, but for the other two data points from your App Registration click the overview button, that will tell you this information outlined in the boxes below:


Running the export

Now we have all the pre-requisites done we can move onto the script to extract the data, so lets get running the code (after you have checked it obviously) you only need to update the variables in bold.

Script : YammerReport.ps1

# Script to extract Yammer reports from Office 365 and email them
# Requires PowerShell 7+ and Microsoft.Graph module

# First, set up base paths and configuration before any other operations
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$baseFolder = "C:\YammerReport"  

# Configuration Variables
$config = @{
    # Azure AD Application Details
    TenantId     = "<tenant_id>"
    ClientId     = "<client_id>"
    ClientSecret = "<client_secret>"
    
    # Paths
    ReportPath    = Join-Path $baseFolder "Reports"
    LogPath       = Join-Path $baseFolder "Logs"
    
    # Email Configuration
    EmailRecipients = @("lee@croucher.cloud","other.user@croucher.cloud")
    SmtpServer    = "mailclaws.bear.local"
    SmtpFrom      = "yammer.report@croucher.cloud"
}

# Create base directories before any logging starts
foreach ($path in @($config.ReportPath, $config.LogPath)) {
    if (!(Test-Path $path)) {
        try {
            New-Item -ItemType Directory -Path $path -Force | Out-Null
            Write-Host "Created directory: $path" -ForegroundColor Green
        }
        catch {
            Write-Host "Failed to create directory $path : $_" -ForegroundColor Red
            exit 1
        }
    }
}

# Function to write verbose log messages
function Write-Log {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Message,
        
        [Parameter(Mandatory=$false)]
        [ValidateSet('Info', 'Warning', 'Error')]
        [string]$Level = 'Info'
    )
    
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "[$timestamp] [$Level] $Message"
    $logFile = Join-Path $config.LogPath "script_log.txt"
    
    # Output to console with color coding
    switch ($Level) {
        'Info'    { Write-Host $logMessage -ForegroundColor Green }
        'Warning' { Write-Host $logMessage -ForegroundColor Yellow }
        'Error'   { Write-Host $logMessage -ForegroundColor Red }
    }
    
    # Write to log file
    try {
        Add-Content -Path $logFile -Value $logMessage
    }
    catch {
        Write-Host "Failed to write to log file: $_" -ForegroundColor Red
    }
}

# Check and install required modules
Write-Log "Checking required PowerShell modules..."
$requiredModules = @(
    @{
        Name = "Microsoft.Graph.Authentication"
        MinimumVersion = "2.24.0"
    },
    @{
        Name = "Microsoft.Graph.Reports"
        MinimumVersion = "2.24.0"
    }
)

# Remove existing Microsoft.Graph modules before importing
Get-Module Microsoft.Graph* | Remove-Module -Force -ErrorAction SilentlyContinue

foreach ($module in $requiredModules) {
    try {
        $installedModule = Get-Module -ListAvailable -Name $module.Name | 
            Where-Object { $_.Version -ge $module.MinimumVersion }
            
        if (-not $installedModule) {
            Write-Log "Installing $($module.Name) module..." -Level Warning
            Install-Module -Name $module.Name -MinimumVersion $module.MinimumVersion -Force -AllowClobber
        }
        
        Import-Module -Name $module.Name -MinimumVersion $module.MinimumVersion -Force
        Write-Log "Successfully imported $($module.Name) version $($installedModule.Version)"
    }
    catch {
        Write-Log "Error checking/importing module $($module.Name): $_" -Level Error
        exit 1
    }
}

# Function to connect to Microsoft Graph
function Connect-ToGraph {
    Write-Log "Attempting to connect to Microsoft Graph..."
    try {
        # Convert ClientSecret to PSCredential
        $secureSecret = ConvertTo-SecureString -String $config.ClientSecret -AsPlainText -Force
        $clientCredential = [System.Management.Automation.PSCredential]::new($config.ClientId, $secureSecret)

        # Use Connect-MgGraph with client credentials
        $params = @{
            Credential = $clientCredential
            TenantId   = $config.TenantId
        }
        
        Connect-MgGraph @params
        Write-Log "Successfully connected to Microsoft Graph"
    }
    catch {
        Write-Log "Failed to connect to Microsoft Graph: $_" -Level Error
        throw
    }
}

# Function to get Yammer reports
function Get-YammerReports {
    Write-Log "Starting Yammer report collection..."
    $reports = @(
        "getYammerActivityUserDetail",
        "getYammerActivityCounts",
        "getYammerDeviceUsageUserDetail",
        "getYammerDeviceUsageDistributionUserCounts",
        "getYammerGroupsActivityDetail",
        "getYammerGroupsActivityGroupCounts"
    )

    $date = Get-Date -Format "yyyy-MM-dd"
    $reportFiles = @()
    
    foreach ($report in $reports) {
        Write-Log "Processing report: $report"
        $reportPath = Join-Path $config.ReportPath "$report-$date.csv"
        
        try {
            $reportData = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/reports/$report(period='D7')" -OutputFilePath $reportPath
            $reportFiles += $reportPath
            Write-Log "Successfully downloaded $report"
        }
        catch {
            Write-Log "Failed to download $report : $_" -Level Error
        }
    }

    return $reportFiles
}

# Function to send email with reports
function Send-ReportEmail {
    param (
        [string[]]$ReportFiles
    )

    Write-Log "Preparing email with $($ReportFiles.Count) attachments..."
    try {
        $emailParams = @{
            From       = $config.SmtpFrom
            To         = $config.EmailRecipients  # Changed from EmailRecipient to EmailRecipients
            Subject    = "Yammer Usage Reports - $(Get-Date -Format 'yyyy-MM-dd')"
            Body      = "Please find attached the Yammer usage reports for $(Get-Date -Format 'yyyy-MM-dd')"
            SmtpServer = $config.SmtpServer
            Attachments = $ReportFiles
        }

        Send-MailMessage @emailParams 
        Write-Log "Email sent successfully"
    }
    catch {
        Write-Log "Failed to send email: $_" -Level Error
        throw
    }
}

# Main execution
Write-Log "=== Starting Yammer Report Script Execution ==="

try {
    Write-Log "Step 1/3: Connecting to Microsoft Graph"
    Connect-ToGraph

    Write-Log "Step 2/3: Collecting Yammer Reports"
    $reportFiles = Get-YammerReports

    Write-Log "Step 3/3: Sending Email"
    Send-ReportEmail -ReportFiles $reportFiles

    Write-Log "Script completed successfully"
}
catch {
    Write-Log "Failed to send email: $_" -Level Error
}
finally {
    if (Get-MgContext) {
        Write-Log "Disconnecting from Microsoft Graph..."
        Disconnect-MgGraph
    }
    Write-Log "=== Script Execution Completed ==="
}

When the script is run it will create a structure as shown below, the folder with the reports is called Reports:


Previous Post Next Post

نموذج الاتصال