The professional standard for production AI deployment
Verify a credentialFor organisationsPartner ProgrammeFor nonprofits & NGOsContact
MSP ToolkitCertified Integrators8 PowerShell scripts

M365 Tenant Discovery.
Scripts. Not guesswork.

Eight production-ready PowerShell scripts that collect everything you need from a Microsoft 365 tenant before any AI engagement starts. Licences, Copilot config, security posture, data governance, automation readiness. One structured output your team can act on.

Why this matters:Competitors charge $8,000–$15,000 AUD for "AI audits" that are essentially structured data collection exercises. These scripts give your engineers the same data in a day — and your assessment fee goes on your expertise in interpreting and acting on it, not the data collection itself.

View all scripts ↓Assessment facilitation guide →

Before you start

Install these PowerShell modules on the engineer machine running the discovery. You need a work account with Global Reader + Security Reader + Exchange View-Only in the client tenant — temporary assignment, remove after engagement.

Microsoft.Graph
Install-Module Microsoft.Graph -Scope CurrentUser
Core Graph API access — users, groups, apps, policies
ExchangeOnlineManagement
Install-Module ExchangeOnlineManagement -Scope CurrentUser
Mailbox config, transport rules, mail flow settings
MicrosoftTeams
Install-Module MicrosoftTeams -Scope CurrentUser
Teams policies, app permissions, meeting settings
Az.Accounts
Install-Module Az.Accounts -Scope CurrentUser
Azure subscription and resource discovery
SharePointPnPPowerShellOnline
Install-Module PnP.PowerShell -Scope CurrentUser
SharePoint site inventory and sensitivity label status

Required tenant permissions

RoleRequired?Notes
Global ReaderRequiredMinimum for read-only discovery. Can be temporary — remove after engagement.
Exchange View-Only Organization ManagementRequiredRequired for Exchange Online discovery.
Teams AdministratorOptionalNeeded if scoping Teams governance. Can use Global Reader for basic Teams config.
SharePoint AdministratorOptionalNeeded for full SharePoint site-level data. PnP requires explicit site access.
Security ReaderRequiredNeeded for Defender, Secure Score, and Conditional Access policy reads.

Discovery scripts

Run these in order. Each script writes output to a discovery-output\ folder in your working directory. Script 08 compiles everything into a client-ready report.

01

Prerequisites setup

Install all required PowerShell modules and connect to the tenant. Run this first — everything else depends on it.

5–10 min📄 Output: Session tokens for Graph, Exchange, Teams. Confirm all modules loaded.
# PAI MSP Toolkit — Prerequisite Setup Script
# Run as: .\01-prereq.ps1 -TenantDomain "contoso.onmicrosoft.com"
# Purpose: Install modules and authenticate to Microsoft 365 tenant

param(
  [Parameter(Mandatory=$true)]
  [string]$TenantDomain,
  [string]$OutputPath = ".\discovery-output"
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

Write-Host "PAI MSP Toolkit — Tenant Discovery Prereq Setup" -ForegroundColor Cyan
Write-Host "Tenant: $TenantDomain" -ForegroundColor Yellow
Write-Host ""

# Create output directory
if (-not (Test-Path $OutputPath)) {
  New-Item -ItemType Directory -Path $OutputPath | Out-Null
  Write-Host "Created output directory: $OutputPath" -ForegroundColor Green
}

# Module installation
$modules = @(
  "Microsoft.Graph",
  "ExchangeOnlineManagement",
  "MicrosoftTeams",
  "Az.Accounts",
  "PnP.PowerShell"
)

foreach ($mod in $modules) {
  if (-not (Get-Module -ListAvailable -Name $mod)) {
    Write-Host "Installing $mod..." -ForegroundColor Yellow
    Install-Module -Name $mod -Scope CurrentUser -Force -AllowClobber
    Write-Host "  $mod installed." -ForegroundColor Green
  } else {
    Write-Host "  $mod already installed." -ForegroundColor Gray
  }
}

# Connect to Microsoft Graph
Write-Host ""
Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan
$scopes = @(
  "Directory.Read.All",
  "Policy.Read.All",
  "AuditLog.Read.All",
  "SecurityEvents.Read.All",
  "Reports.Read.All",
  "RoleManagement.Read.Directory",
  "Organization.Read.All",
  "User.Read.All",
  "Group.Read.All",
  "Application.Read.All",
  "DeviceManagementConfiguration.Read.All"
)
Connect-MgGraph -Scopes $scopes -TenantId $TenantDomain

# Connect to Exchange Online
Write-Host "Connecting to Exchange Online..." -ForegroundColor Cyan
Connect-ExchangeOnline -Organization $TenantDomain -ShowBanner:$false

# Connect to Teams
Write-Host "Connecting to Microsoft Teams..." -ForegroundColor Cyan
Connect-MicrosoftTeams -TenantId $TenantDomain | Out-Null

Write-Host ""
Write-Host "All connections established. Ready to run discovery scripts." -ForegroundColor Green
Write-Host "Output will be written to: $OutputPath" -ForegroundColor Yellow
02

Tenant profile and licensing

Collect the tenant's licence inventory, SKUs, assigned vs available seats, and identify which Microsoft AI products are licensed.

2–5 min📄 Output: CSV: licence inventory with AI-relevant SKUs highlighted. JSON: tenant org profile.
# PAI MSP Toolkit — Tenant Profile and Licensing
# Run as: .\02-tenant-profile.ps1 -OutputPath ".\discovery-output"
# Requires: Microsoft.Graph connected (run 01-prereq.ps1 first)

param(
  [string]$OutputPath = ".\discovery-output"
)

Set-StrictMode -Version Latest
Write-Host "Script 02: Tenant Profile and Licensing" -ForegroundColor Cyan

# AI-relevant SKU patterns to flag
$aiSkuPatterns = @(
  "COPILOT",
  "M365_COPILOT",
  "MICROSOFT_365_COPILOT",
  "VIVA",
  "POWER_AUTOMATE",
  "POWER_BI",
  "AZURE_AI",
  "COGNITIVE_SERVICES",
  "OPENAI",
  "SYNTEX"
)

# Org profile
$org = Get-MgOrganization
$orgProfile = [PSCustomObject]@{
  TenantId         = $org.Id
  DisplayName      = $org.DisplayName
  Domain           = ($org.VerifiedDomains | Where-Object { $_.IsDefault }).Name
  Country          = $org.CountryLetterCode
  CreatedDateTime  = $org.CreatedDateTime
  TenantType       = $org.TenantType
}
$orgProfile | ConvertTo-Json | Out-File "$OutputPath	enant-profile.json" -Encoding UTF8
Write-Host "  Tenant: $($org.DisplayName) | $($orgProfile.Domain)" -ForegroundColor Green

# Total user count
$userCount = (Get-MgUser -Count -ConsistencyLevel eventual -Filter "accountEnabled eq true").Length
Write-Host "  Active users: $userCount" -ForegroundColor Green

# Licence inventory
Write-Host "  Collecting licence data..." -ForegroundColor Yellow
$subscriptions = Get-MgSubscribedSku
$licenceRows = @()

foreach ($sub in $subscriptions) {
  $skuName = $sub.SkuPartNumber
  $isAiRelevant = $false
  foreach ($pattern in $aiSkuPatterns) {
    if ($skuName -like "*$pattern*") { $isAiRelevant = $true; break }
  }

  $licenceRows += [PSCustomObject]@{
    SKU              = $skuName
    FriendlyName     = $sub.SkuId  # Graph doesn't return friendly name; map manually
    TotalLicences    = $sub.PrepaidUnits.Enabled
    AssignedLicences = $sub.ConsumedUnits
    AvailableLicences = ($sub.PrepaidUnits.Enabled - $sub.ConsumedUnits)
    CapabilityStatus  = $sub.CapabilityStatus
    AIRelevant        = $isAiRelevant
  }
}

$licenceRows | Export-Csv "$OutputPathlicence-inventory.csv" -NoTypeInformation
Write-Host "  Licence inventory saved. AI-relevant SKUs found:" -ForegroundColor Green
$licenceRows | Where-Object { $_.AIRelevant } | ForEach-Object {
  Write-Host "    * $($_.SKU): $($_.AssignedLicences) / $($_.TotalLicences) assigned" -ForegroundColor Magenta
}

Write-Host "Script 02 complete." -ForegroundColor Green
03

Copilot configuration and AI tool footprint

Discover Copilot for M365 enablement status, which users have it assigned, Copilot Studio agents in use, and any third-party AI apps installed via the Teams/M365 app store.

5–10 min📄 Output: CSV: Copilot-licensed users. CSV: third-party AI apps. JSON: Copilot Studio agents.
# PAI MSP Toolkit — Copilot Configuration and AI Tool Footprint
# Run as: .\03-copilot-config.ps1 -OutputPath ".\discovery-output"
# Requires: Microsoft.Graph connected

param(
  [string]$OutputPath = ".\discovery-output"
)

Set-StrictMode -Version Latest
Write-Host "Script 03: Copilot Config and AI Tool Footprint" -ForegroundColor Cyan

# Known AI app IDs in Teams/M365 ecosystem
$knownAiApps = @{
  "1542629d-7a94-4f03-9dab-da58c0c0acfd" = "Microsoft Copilot (Teams)"
  "com.microsoft.teamspace.tab.copilot"   = "Copilot in Teams"
  "1f1b1bef-a052-4df4-b655-fb9f1843d1cc" = "Copilot Studio"
  "aa3f4303-9640-45cb-9fd4-26e7e3adc8ef" = "Power Automate"
  "2e24e22c-1ad3-4f30-96b4-8d0ef77a2b3c" = "Power BI"
  "0d820ecd-def2-4297-adad-78056cde7c78" = "Azure AI Foundry"
}

# Copilot SKUs
$copilotSkus = @(
  "Microsoft_365_Copilot",
  "M365_COPILOT",
  "COPILOT_FOR_MICROSOFT_365"
)

Write-Host "  Finding Copilot-licensed users..." -ForegroundColor Yellow

# Get all users with Copilot licence
$copilotUsers = @()
$allUsers = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,AssignedLicenses,Department,JobTitle,AccountEnabled |
  Where-Object { $_.AccountEnabled }

$skus = Get-MgSubscribedSku
$copilotSkuIds = $skus |
  Where-Object { $copilotSkus -contains $_.SkuPartNumber -or $_.SkuPartNumber -like "*COPILOT*" } |
  Select-Object -ExpandProperty SkuId

foreach ($user in $allUsers) {
  $hasCopilot = $user.AssignedLicenses | Where-Object { $copilotSkuIds -contains $_.SkuId }
  if ($hasCopilot) {
    $copilotUsers += [PSCustomObject]@{
      DisplayName       = $user.DisplayName
      UserPrincipalName = $user.UserPrincipalName
      Department        = $user.Department
      JobTitle          = $user.JobTitle
      CopilotAssigned   = $true
    }
  }
}

$copilotUsers | Export-Csv "$OutputPathcopilot-licensed-users.csv" -NoTypeInformation
Write-Host "  Copilot licensed users: $($copilotUsers.Count)" -ForegroundColor Green

# Discover Teams apps (AI footprint)
Write-Host "  Scanning Teams app catalogue for AI tools..." -ForegroundColor Yellow

try {
  $teamsApps = Get-MgTeamApp -Filter "distributionMethod eq 'organization'" -All 2>$null
  $aiAppsFound = @()
  foreach ($app in $teamsApps) {
    $aiAppsFound += [PSCustomObject]@{
      AppId        = $app.Id
      DisplayName  = $app.DisplayName
      ExternalId   = $app.ExternalId
      IsKnownAI    = ($knownAiApps.ContainsKey($app.Id) -or $knownAiApps.ContainsKey($app.ExternalId))
      Category     = if ($knownAiApps.ContainsKey($app.Id)) { $knownAiApps[$app.Id] } else { "Unknown" }
    }
  }
  $aiAppsFound | Export-Csv "$OutputPath	eams-ai-apps.csv" -NoTypeInformation
  Write-Host "  Teams apps scanned: $($teamsApps.Count) total" -ForegroundColor Green
  Write-Host "  Known AI apps found: $($aiAppsFound | Where-Object { $_.IsKnownAI } | Measure-Object | Select-Object -Exp Count)" -ForegroundColor Magenta
} catch {
  Write-Host "  Teams app scan requires Teams Admin access — skipping." -ForegroundColor Yellow
}

# Power Platform — check if any flows exist (indicator of automation maturity)
Write-Host "  Checking Power Platform signal..." -ForegroundColor Yellow
# Note: Full Power Platform inventory requires Power Platform Admin Centre API
# This checks for licensed users as a proxy
$ppSkus = $skus | Where-Object { $_.SkuPartNumber -like "*POWER*" }
if ($ppSkus) {
  Write-Host "  Power Platform licences detected:" -ForegroundColor Green
  $ppSkus | ForEach-Object { Write-Host "    * $($_.SkuPartNumber): $($_.ConsumedUnits) assigned" -ForegroundColor Magenta }
} else {
  Write-Host "  No Power Platform licences found." -ForegroundColor Gray
}

Write-Host "Script 03 complete." -ForegroundColor Green
04

Security posture and Conditional Access

Assess MFA adoption, Conditional Access policies, Secure Score, privileged role assignments, and guest access configuration — all factors that gate safe AI deployment.

5–10 min📄 Output: JSON: Secure Score breakdown. CSV: privileged role members. CSV: Conditional Access policy summary.
# PAI MSP Toolkit — Security Posture Assessment
# Run as: .\04-security-posture.ps1 -OutputPath ".\discovery-output"
# Requires: Microsoft.Graph connected with SecurityEvents.Read.All

param(
  [string]$OutputPath = ".\discovery-output"
)

Set-StrictMode -Version Latest
Write-Host "Script 04: Security Posture and Conditional Access" -ForegroundColor Cyan

# Microsoft Secure Score
Write-Host "  Collecting Secure Score..." -ForegroundColor Yellow
try {
  $secureScores = Get-MgSecuritySecureScore -Top 1
  if ($secureScores) {
    $score = $secureScores[0]
    $secureScoreSummary = [PSCustomObject]@{
      CurrentScore       = $score.CurrentScore
      MaxScore           = $score.MaxScore
      ScorePercentage    = [math]::Round(($score.CurrentScore / $score.MaxScore) * 100, 1)
      AverageComparativeScore = ($score.AverageComparativeScores | Where-Object { $_.Basis -eq "AllTenants" }).AverageScore
      CreatedDateTime    = $score.CreatedDateTime
    }
    $secureScoreSummary | ConvertTo-Json | Out-File "$OutputPathsecure-score.json" -Encoding UTF8
    Write-Host "  Secure Score: $($secureScoreSummary.CurrentScore) / $($secureScoreSummary.MaxScore) ($($secureScoreSummary.ScorePercentage)%)" -ForegroundColor Green
  }
} catch {
  Write-Host "  Secure Score requires Security Reader role — skipping." -ForegroundColor Yellow
}

# Conditional Access Policies
Write-Host "  Collecting Conditional Access policies..." -ForegroundColor Yellow
$caPolicies = Get-MgIdentityConditionalAccessPolicy -All
$caRows = $caPolicies | ForEach-Object {
  [PSCustomObject]@{
    DisplayName   = $_.DisplayName
    State         = $_.State
    CreatedAt     = $_.CreatedDateTime
    ModifiedAt    = $_.ModifiedDateTime
    UsersIncluded = ($_.Conditions.Users.IncludeUsers -join ", ")
    UsersExcluded = ($_.Conditions.Users.ExcludeUsers -join ", ")
    AppsIncluded  = ($_.Conditions.Applications.IncludeApplications -join ", ")
    MFARequired   = ($_.GrantControls.BuiltInControls -contains "mfa")
    BlockAccess   = ($_.GrantControls.Operator -eq "Block" -or $_.GrantControls.BuiltInControls -contains "block")
  }
}
$caRows | Export-Csv "$OutputPathconditional-access-policies.csv" -NoTypeInformation
Write-Host "  CA Policies found: $($caPolicies.Count) ($($caRows | Where-Object { $_.State -eq 'enabled' } | Measure-Object | Select-Object -Exp Count) enabled)" -ForegroundColor Green
Write-Host "  Policies requiring MFA: $($caRows | Where-Object { $_.MFARequired } | Measure-Object | Select-Object -Exp Count)" -ForegroundColor Green

# Privileged Role Assignments
Write-Host "  Collecting privileged role members..." -ForegroundColor Yellow
$highPrivRoles = @(
  "62e90394-69f5-4237-9190-012177145e10", # Global Administrator
  "194ae4cb-b126-40b2-bd5b-6091b380977d", # Security Administrator
  "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3", # Application Administrator
  "c4e39bd9-1100-46d3-8c65-fb160da0071f", # Authentication Administrator
  "729827e3-9c14-49f7-bb1b-9608f156bbb8"  # Helpdesk Administrator
)
$roleRows = @()
foreach ($roleId in $highPrivRoles) {
  try {
    $roleAssignments = Get-MgDirectoryRoleMember -DirectoryRoleId $roleId -ErrorAction SilentlyContinue
    if ($roleAssignments) {
      $roleDef = Get-MgDirectoryRole -DirectoryRoleId $roleId -ErrorAction SilentlyContinue
      foreach ($member in $roleAssignments) {
        $roleRows += [PSCustomObject]@{
          RoleName   = $roleDef.DisplayName
          MemberId   = $member.Id
          MemberType = $member.AdditionalProperties.'@odata.type'
        }
      }
    }
  } catch { }
}
$roleRows | Export-Csv "$OutputPathprivileged-roles.csv" -NoTypeInformation
Write-Host "  Privileged role members found: $($roleRows.Count)" -ForegroundColor Green

$globalAdmins = $roleRows | Where-Object { $_.RoleName -eq "Global Administrator" }
if ($globalAdmins.Count -gt 5) {
  Write-Host "  WARNING: $($globalAdmins.Count) Global Administrators found — recommend reducing." -ForegroundColor Red
}

# Guest access configuration
Write-Host "  Checking guest access settings..." -ForegroundColor Yellow
$authPolicy = Get-MgPolicyAuthorizationPolicy
$guestSettings = [PSCustomObject]@{
  AllowInvites          = $authPolicy.AllowInvitesFrom
  GuestUserRole         = $authPolicy.GuestUserRoleId
  BlockMsolPowerShell   = $authPolicy.BlockMsolPowerShell
}
$guestSettings | ConvertTo-Json | Out-File "$OutputPathguest-access-settings.json" -Encoding UTF8
Write-Host "  Guest invite policy: $($authPolicy.AllowInvitesFrom)" -ForegroundColor Green

Write-Host "Script 04 complete." -ForegroundColor Green
05

Data governance and sensitivity labels

Inventory Microsoft Purview sensitivity labels, DLP policies, SharePoint external sharing settings, and whether data classification is in place before AI can safely access content.

5–10 min📄 Output: CSV: sensitivity label list. CSV: DLP policies. JSON: SharePoint external sharing summary.
# PAI MSP Toolkit — Data Governance and Sensitivity Labels
# Run as: .\05-data-governance.ps1 -OutputPath ".\discovery-output"
# Requires: Microsoft.Graph connected

param(
  [string]$OutputPath = ".\discovery-output"
)

Set-StrictMode -Version Latest
Write-Host "Script 05: Data Governance and Sensitivity Labels" -ForegroundColor Cyan

# Sensitivity Labels via Microsoft Graph
Write-Host "  Collecting sensitivity labels from Microsoft Purview..." -ForegroundColor Yellow
try {
  $labels = Get-MgSecurityInformationProtectionSensitivityLabel -All
  $labelRows = $labels | ForEach-Object {
    [PSCustomObject]@{
      Id           = $_.Id
      Name         = $_.Name
      Description  = $_.Description
      Color        = $_.Color
      Sensitivity  = $_.Sensitivity
      IsActive     = $_.IsActive
      IsEndpointProtectionEnabled = $_.IsEndpointProtectionEnabled
    }
  }
  $labelRows | Export-Csv "$OutputPathsensitivity-labels.csv" -NoTypeInformation
  Write-Host "  Sensitivity labels defined: $($labels.Count)" -ForegroundColor Green
  if ($labels.Count -eq 0) {
    Write-Host "  WARNING: No sensitivity labels found. Data classification not configured — HIGH RISK for AI deployment." -ForegroundColor Red
  }
} catch {
  Write-Host "  Label enumeration requires Compliance Reader — attempting Exchange method..." -ForegroundColor Yellow
}

# DLP Policies via Exchange Online (compliance centre)
Write-Host "  Collecting DLP policies..." -ForegroundColor Yellow
try {
  $dlpPolicies = Get-DlpCompliancePolicy -All
  $dlpRows = $dlpPolicies | ForEach-Object {
    [PSCustomObject]@{
      Name           = $_.Name
      Mode           = $_.Mode
      Workload       = $_.Workload
      IsEnabled      = $_.Enabled
      CreatedAt      = $_.WhenCreated
      ModifiedAt     = $_.WhenChanged
    }
  }
  $dlpRows | Export-Csv "$OutputPathdlp-policies.csv" -NoTypeInformation
  Write-Host "  DLP policies found: $($dlpPolicies.Count)" -ForegroundColor Green

  $activeDlp = $dlpRows | Where-Object { $_.IsEnabled }
  Write-Host "  Active DLP policies: $($activeDlp.Count)" -ForegroundColor Green
  if ($activeDlp.Count -eq 0) {
    Write-Host "  WARNING: No active DLP policies. Data loss prevention not configured." -ForegroundColor Red
  }
} catch {
  Write-Host "  DLP policy read requires Compliance Admin role — skipping." -ForegroundColor Yellow
}

# SharePoint External Sharing
Write-Host "  Checking SharePoint external sharing settings..." -ForegroundColor Yellow
try {
  $spTenant = Get-SPOTenant 2>$null
  if ($spTenant) {
    $spSettings = [PSCustomObject]@{
      SharingCapability         = $spTenant.SharingCapability
      DefaultSharingLinkType    = $spTenant.DefaultSharingLinkType
      ExternalServicesEnabled   = $spTenant.ExternalServicesEnabled
      AllowEditing              = $spTenant.AllowEditing
      RequireAcceptingAccount   = $spTenant.RequireAcceptingAccountMatchInvitedAccount
    }
    $spSettings | ConvertTo-Json | Out-File "$OutputPathsharepoint-sharing.json" -Encoding UTF8

    if ($spTenant.SharingCapability -eq "ExternalUserAndGuestSharing") {
      Write-Host "  WARNING: SharePoint sharing set to Anyone (highest risk). Review before AI deployment." -ForegroundColor Red
    } else {
      Write-Host "  SharePoint sharing: $($spTenant.SharingCapability)" -ForegroundColor Green
    }
  }
} catch {
  Write-Host "  SharePoint tenant config requires SharePoint Admin — checking via Graph instead..." -ForegroundColor Yellow
  $settings = Get-MgAdminSharepoint | Select-Object * -ErrorAction SilentlyContinue
  if ($settings) { $settings | ConvertTo-Json | Out-File "$OutputPathsharepoint-sharing.json" -Encoding UTF8 }
}

# Retention Policies
Write-Host "  Collecting retention policies..." -ForegroundColor Yellow
try {
  $retPolicies = Get-RetentionCompliancePolicy -All
  $retRows = $retPolicies | ForEach-Object {
    [PSCustomObject]@{
      Name      = $_.Name
      Enabled   = $_.Enabled
      Workload  = $_.Workload
      Created   = $_.WhenCreated
    }
  }
  $retRows | Export-Csv "$OutputPath
etention-policies.csv" -NoTypeInformation
  Write-Host "  Retention policies: $($retPolicies.Count)" -ForegroundColor Green
} catch {
  Write-Host "  Retention policy read skipped (requires Compliance Admin)." -ForegroundColor Yellow
}

Write-Host "Script 05 complete." -ForegroundColor Green
06

User activity and productivity signals

Collect M365 usage reports to understand collaboration patterns, application adoption, and identify which teams would benefit most from AI tooling.

5–10 min📄 Output: CSV: M365 app usage by user. CSV: Teams activity summary. CSV: email volume by department.
# PAI MSP Toolkit — User Activity and Productivity Signals
# Run as: .\06-user-activity.ps1 -OutputPath ".\discovery-output"
# Requires: Reports.Read.All permission

param(
  [string]$OutputPath = ".\discovery-output",
  [int]$PeriodDays = 30
)

Set-StrictMode -Version Latest
Write-Host "Script 06: User Activity and Productivity Signals" -ForegroundColor Cyan
Write-Host "  Period: last $PeriodDays days" -ForegroundColor Yellow

$period = "D$PeriodDays"

# M365 Apps Usage Detail
Write-Host "  Collecting M365 app usage..." -ForegroundColor Yellow
try {
  $m365Usage = Get-MgReportM365AppUserDetail -Period $period -OutFile "$OutputPathm365-app-usage-raw.csv"
  Write-Host "  M365 app usage report saved." -ForegroundColor Green
} catch {
  Write-Host "  M365 usage report: $($_.Exception.Message)" -ForegroundColor Yellow
}

# Teams User Activity
Write-Host "  Collecting Teams activity..." -ForegroundColor Yellow
try {
  Get-MgReportTeamUserActivityUserDetail -Period $period -OutFile "$OutputPath	eams-user-activity-raw.csv"
  Write-Host "  Teams activity report saved." -ForegroundColor Green
} catch {
  Write-Host "  Teams activity: $($_.Exception.Message)" -ForegroundColor Yellow
}

# Email activity
Write-Host "  Collecting email activity..." -ForegroundColor Yellow
try {
  Get-MgReportEmailActivityUserDetail -Period $period -OutFile "$OutputPathemail-activity-raw.csv"
  Write-Host "  Email activity report saved." -ForegroundColor Green
} catch {
  Write-Host "  Email activity: $($_.Exception.Message)" -ForegroundColor Yellow
}

# SharePoint activity
Write-Host "  Collecting SharePoint activity..." -ForegroundColor Yellow
try {
  Get-MgReportSharePointActivityUserDetail -Period $period -OutFile "$OutputPathsharepoint-activity-raw.csv"
  Write-Host "  SharePoint activity report saved." -ForegroundColor Green
} catch {
  Write-Host "  SharePoint activity: $($_.Exception.Message)" -ForegroundColor Yellow
}

# OneDrive usage
Write-Host "  Collecting OneDrive usage..." -ForegroundColor Yellow
try {
  Get-MgReportOneDriveUsageAccountDetail -Period $period -OutFile "$OutputPathonedrive-usage-raw.csv"
  Write-Host "  OneDrive usage report saved." -ForegroundColor Green
} catch {
  Write-Host "  OneDrive usage: $($_.Exception.Message)" -ForegroundColor Yellow
}

# Summarise by department — parse the M365 usage CSV
Write-Host "  Building department activity summary..." -ForegroundColor Yellow
if (Test-Path "$OutputPathm365-app-usage-raw.csv") {
  $usageData = Import-Csv "$OutputPathm365-app-usage-raw.csv"

  # Enrich with department from Azure AD
  $userDepts = @{}
  Get-MgUser -All -Property UserPrincipalName,Department | ForEach-Object {
    $userDepts[$_.UserPrincipalName] = $_.Department
  }

  $summary = $usageData | Group-Object { $userDepts[$_.'User Principal Name'] } | ForEach-Object {
    $dept = if ($_.Name) { $_.Name } else { "Unknown" }
    $users = $_.Group
    [PSCustomObject]@{
      Department     = $dept
      UserCount      = $users.Count
      TeamsActive    = ($users | Where-Object { $_.'Microsoft Teams' -eq 'Yes' } | Measure-Object).Count
      OutlookActive  = ($users | Where-Object { $_.'Outlook' -eq 'Yes' } | Measure-Object).Count
      SPActive       = ($users | Where-Object { $_.'SharePoint' -eq 'Yes' } | Measure-Object).Count
      OneDriveActive = ($users | Where-Object { $_.'OneDrive' -eq 'Yes' } | Measure-Object).Count
      WordActive     = ($users | Where-Object { $_.'Word' -eq 'Yes' } | Measure-Object).Count
      ExcelActive    = ($users | Where-Object { $_.'Excel' -eq 'Yes' } | Measure-Object).Count
    }
  }
  $summary | Export-Csv "$OutputPathdepartment-activity-summary.csv" -NoTypeInformation
  Write-Host "  Department summary saved. Top 3 departments by Teams adoption:" -ForegroundColor Green
  $summary | Sort-Object TeamsActive -Descending | Select-Object -First 3 | ForEach-Object {
    Write-Host "    $($_.Department): $($_.TeamsActive)/$($_.UserCount) Teams-active" -ForegroundColor Magenta
  }
}

Write-Host "Script 06 complete." -ForegroundColor Green
07

Automation readiness — Power Platform and connectors

Check Power Automate environment config, existing flows (where permitted), connector policies, and signal the highest-ROI automation opportunities based on what's already in use.

5–10 min📄 Output: JSON: Power Platform environment summary. CSV: connector policy list. TXT: automation opportunity signals.
# PAI MSP Toolkit — Automation Readiness
# Run as: .\07-automation-readiness.ps1 -OutputPath ".\discovery-output"
# Requires: Power Platform Admin rights (or Global Reader for policy-only view)

param(
  [string]$OutputPath = ".\discovery-output",
  [string]$TenantDomain = ""
)

Set-StrictMode -Version Latest
Write-Host "Script 07: Automation Readiness — Power Platform" -ForegroundColor Cyan

# Check if Power Platform Admin module available
$ppModule = Get-Module -ListAvailable -Name "Microsoft.PowerApps.Administration.PowerShell"
if (-not $ppModule) {
  Write-Host "  Installing Power Platform Admin module..." -ForegroundColor Yellow
  Install-Module -Name Microsoft.PowerApps.Administration.PowerShell -Scope CurrentUser -Force -AllowClobber
  Install-Module -Name Microsoft.PowerApps.PowerShell -Scope CurrentUser -Force -AllowClobber
}

try {
  Add-PowerAppsAccount -Endpoint "prod"

  # Environment inventory
  Write-Host "  Collecting Power Platform environments..." -ForegroundColor Yellow
  $envs = Get-AdminPowerAppEnvironment
  $envRows = $envs | ForEach-Object {
    [PSCustomObject]@{
      DisplayName       = $_.DisplayName
      EnvironmentName   = $_.EnvironmentName
      Type              = $_.EnvironmentType
      Region            = $_.Location
      IsDefault         = $_.IsDefault
      CreatedBy         = $_.CreatedBy.displayName
      CreatedTime       = $_.CreatedTime
      DatabaseExists    = $_.CommonDataServiceDatabaseProvisioningState -eq "Succeeded"
    }
  }
  $envRows | Export-Csv "$OutputPathpower-platform-environments.csv" -NoTypeInformation
  Write-Host "  Environments found: $($envs.Count)" -ForegroundColor Green

  # Data Loss Prevention (Connector) Policies
  Write-Host "  Collecting connector policies (DLP)..." -ForegroundColor Yellow
  $dlpPolicies = Get-AdminDlpPolicy
  $dlpRows = $dlpPolicies | ForEach-Object {
    [PSCustomObject]@{
      PolicyName   = $_.DisplayName
      EnvironScope = $_.EnvironmentName
      CreatedTime  = $_.CreatedTime
      Type         = $_.PolicyType
    }
  }
  $dlpRows | Export-Csv "$OutputPathpower-platform-dlp-policies.csv" -NoTypeInformation
  Write-Host "  Power Platform DLP policies: $($dlpPolicies.Count)" -ForegroundColor Green

  if ($dlpPolicies.Count -eq 0) {
    Write-Host "  WARNING: No Power Platform DLP policies configured — connectors are unrestricted." -ForegroundColor Red
  }

  # Flow count per environment (proxy for automation maturity)
  Write-Host "  Counting flows per environment..." -ForegroundColor Yellow
  $flowSummary = @()
  foreach ($env in $envs | Select-Object -First 5) {
    try {
      $flows = Get-AdminFlow -EnvironmentName $env.EnvironmentName
      $flowSummary += [PSCustomObject]@{
        Environment = $env.DisplayName
        FlowCount   = $flows.Count
        EnabledFlows = ($flows | Where-Object { $_.Properties.state -eq "Started" } | Measure-Object).Count
      }
    } catch { }
  }
  $flowSummary | Export-Csv "$OutputPathpower-automate-flow-summary.csv" -NoTypeInformation

} catch {
  Write-Host "  Power Platform Admin access not available — generating signal from licences only." -ForegroundColor Yellow

  # Fallback: infer from Graph licence data
  if (Test-Path "$OutputPathlicence-inventory.csv") {
    $licences = Import-Csv "$OutputPathlicence-inventory.csv"
    $ppLicences = $licences | Where-Object { $_.SKU -like "*POWER*" -or $_.SKU -like "*FLOW*" }

    $signals = @()
    if ($ppLicences | Where-Object { $_.SKU -like "*POWER_AUTOMATE*" }) {
      $signals += "Power Automate licences detected — automation capability exists"
    }
    if ($ppLicences | Where-Object { $_.SKU -like "*POWER_BI*" }) {
      $signals += "Power BI licences detected — data reporting capability exists"
    }

    $signals | Out-File "$OutputPathautomation-signals.txt" -Encoding UTF8
    Write-Host "  Automation signals written from licence data." -ForegroundColor Green
  }
}

# Generate automation opportunity map based on what we know
Write-Host "  Generating automation opportunity map..." -ForegroundColor Yellow
$opportunities = @"
AUTOMATION OPPORTUNITY MAP — Generated $(Get-Date -Format 'yyyy-MM-dd')
Based on tenant discovery data. Review each signal with the client.

HIGH CONFIDENCE OPPORTUNITIES (based on tool presence):
- Email triage and routing (requires: Exchange Online + Power Automate)
- Document approval workflows (requires: SharePoint + Power Automate)
- Teams notifications for business events (requires: Teams + Power Automate)
- Copilot-assisted meeting summaries (requires: Copilot for M365 licence)
- SharePoint document classification (requires: Purview sensitivity labels)

SIGNALS TO INVESTIGATE IN STAKEHOLDER INTERVIEWS:
- What manual data entry tasks happen repeatedly each week?
- Which approval processes still run via email chains?
- Are there reports generated manually from spreadsheets?
- What does onboarding/offboarding look like step-by-step?
- Which teams spend the most time finding information across systems?

RECOMMENDED COPILOT PILOT TARGETS:
- Departments with highest Teams and Outlook activity (see department-activity-summary.csv)
- Roles with document-heavy workflows (legal, finance, HR, operations)
- Teams that do frequent meeting-heavy scheduling
"@
$opportunities | Out-File "$OutputPathautomation-opportunity-map.txt" -Encoding UTF8
Write-Host "  Automation opportunity map saved." -ForegroundColor Green

Write-Host "Script 07 complete." -ForegroundColor Green
08

Compile discovery report

Aggregate all discovery output into a structured JSON report and generate a human-readable HTML summary ready to review with the client.

2–5 min📄 Output: JSON: discovery-report.json. HTML: discovery-summary.html (client-ready).
# PAI MSP Toolkit — Compile Discovery Report
# Run as: .\08-compile-report.ps1 -TenantName "Contoso" -OutputPath ".\discovery-output"
# Requires: All previous scripts to have run successfully

param(
  [Parameter(Mandatory=$true)]
  [string]$TenantName,
  [string]$OutputPath = ".\discovery-output",
  [string]$AssessorName = "MSP Engineer",
  [string]$AssessorFirm = "Your MSP Firm"
)

Set-StrictMode -Version Latest
Write-Host "Script 08: Compiling Discovery Report" -ForegroundColor Cyan

$reportDate = Get-Date -Format "yyyy-MM-dd"
$report = @{ TenantName = $TenantName; AssessmentDate = $reportDate; AssessorName = $AssessorName; AssessorFirm = $AssessorFirm; Sections = @{} }

# Load each output file
if (Test-Path "$OutputPath	enant-profile.json") {
  $report.Sections.TenantProfile = Get-Content "$OutputPath	enant-profile.json" | ConvertFrom-Json
}
if (Test-Path "$OutputPathlicence-inventory.csv") {
  $licences = Import-Csv "$OutputPathlicence-inventory.csv"
  $report.Sections.Licencing = @{
    TotalSKUs       = $licences.Count
    AIRelevantSKUs  = @($licences | Where-Object { $_.AIRelevant -eq "True" })
    TotalSeats      = ($licences | Measure-Object -Property TotalLicences -Sum).Sum
    AssignedSeats   = ($licences | Measure-Object -Property AssignedLicences -Sum).Sum
  }
}
if (Test-Path "$OutputPathcopilot-licensed-users.csv") {
  $copilot = Import-Csv "$OutputPathcopilot-licensed-users.csv"
  $report.Sections.CopilotFootprint = @{ LicensedUserCount = $copilot.Count; Users = @($copilot) }
}
if (Test-Path "$OutputPathsecure-score.json") {
  $report.Sections.SecurityPosture = Get-Content "$OutputPathsecure-score.json" | ConvertFrom-Json
}
if (Test-Path "$OutputPathconditional-access-policies.csv") {
  $ca = Import-Csv "$OutputPathconditional-access-policies.csv"
  $report.Sections.ConditionalAccess = @{
    TotalPolicies   = $ca.Count
    EnabledPolicies = @($ca | Where-Object { $_.State -eq "enabled" }).Count
    MFAPolicies     = @($ca | Where-Object { $_.MFARequired -eq "True" }).Count
  }
}
if (Test-Path "$OutputPathsensitivity-labels.csv") {
  $labels = Import-Csv "$OutputPathsensitivity-labels.csv"
  $report.Sections.DataGovernance = @{ SensitivityLabelCount = $labels.Count; Labels = @($labels) }
}
if (Test-Path "$OutputPathautomation-opportunity-map.txt") {
  $report.Sections.AutomationSignals = Get-Content "$OutputPathautomation-opportunity-map.txt" -Raw
}

# RAG risk scores
$risks = @()
if ($report.Sections.DataGovernance.SensitivityLabelCount -eq 0) {
  $risks += @{ Area = "Data Classification"; Status = "RED"; Detail = "No sensitivity labels configured — AI deployment blocked until resolved" }
} elseif ($report.Sections.DataGovernance.SensitivityLabelCount -lt 3) {
  $risks += @{ Area = "Data Classification"; Status = "AMBER"; Detail = "Only $($report.Sections.DataGovernance.SensitivityLabelCount) labels — expand before Copilot rollout" }
} else {
  $risks += @{ Area = "Data Classification"; Status = "GREEN"; Detail = "$($report.Sections.DataGovernance.SensitivityLabelCount) labels configured" }
}
if ($report.Sections.ConditionalAccess.MFAPolicies -eq 0) {
  $risks += @{ Area = "MFA Coverage"; Status = "RED"; Detail = "No CA policies enforce MFA — critical security gap" }
} elseif ($report.Sections.ConditionalAccess.MFAPolicies -lt 2) {
  $risks += @{ Area = "MFA Coverage"; Status = "AMBER"; Detail = "MFA not universally enforced" }
} else {
  $risks += @{ Area = "MFA Coverage"; Status = "GREEN"; Detail = "$($report.Sections.ConditionalAccess.MFAPolicies) MFA-enforcing policies active" }
}
$report.RiskRegister = $risks

# Save JSON report
$report | ConvertTo-Json -Depth 10 | Out-File "$OutputPathdiscovery-report.json" -Encoding UTF8
Write-Host "  Discovery report saved: $OutputPathdiscovery-report.json" -ForegroundColor Green

# Generate HTML summary
$redCount   = ($risks | Where-Object { $_.Status -eq "RED" }).Count
$amberCount = ($risks | Where-Object { $_.Status -eq "AMBER" }).Count
$greenCount = ($risks | Where-Object { $_.Status -eq "GREEN" }).Count

$html = @"
<!DOCTYPE html><html><head><meta charset='UTF-8'>
<title>AI Readiness Discovery — $TenantName</title>
<style>
  body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; max-width: 960px; margin: 40px auto; padding: 0 24px; color: #1e293b; }
  h1 { font-size: 28px; font-weight: 800; } h2 { font-size: 18px; font-weight: 700; margin-top: 32px; }
  .meta { color: #64748b; font-size: 14px; margin-bottom: 32px; }
  .grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; margin: 16px 0; }
  .card { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 16px 20px; }
  .num { font-size: 28px; font-weight: 800; }
  table { width: 100%; border-collapse: collapse; margin: 12px 0; }
  th { text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: #64748b; padding: 8px 12px; border-bottom: 2px solid #e2e8f0; }
  td { padding: 10px 12px; border-bottom: 1px solid #f1f5f9; font-size: 14px; }
  .red { color: #dc2626; font-weight: 700; } .amber { color: #d97706; font-weight: 700; } .green { color: #16a34a; font-weight: 700; }
  .badge { display:inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; }
  .badge.red { background: #fef2f2; color: #dc2626; } .badge.amber { background: #fffbeb; color: #d97706; } .badge.green { background: #f0fdf4; color: #16a34a; }
  footer { margin-top: 48px; font-size: 12px; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 16px; }
</style></head><body>
<h1>AI Readiness Discovery Report</h1>
<div class='meta'>$TenantName &nbsp;·&nbsp; $reportDate &nbsp;·&nbsp; Prepared by $AssessorName, $AssessorFirm</div>

<h2>Risk Summary</h2>
<div class='grid'>
  <div class='card'><div class='num red'>$redCount</div><div>Critical findings</div></div>
  <div class='card'><div class='num amber'>$amberCount</div><div>Moderate findings</div></div>
  <div class='card'><div class='num green'>$greenCount</div><div>Items passing</div></div>
</div>

<h2>Risk Register</h2>
<table><tr><th>Area</th><th>Status</th><th>Detail</th></tr>
$($risks | ForEach-Object { "<tr><td>$($_.Area)</td><td><span class='badge $($_.Status.ToLower())'>$($_.Status)</span></td><td>$($_.Detail)</td></tr>" } | Out-String)
</table>

<h2>Licence Summary</h2>
<p>Total licensed seats: <strong>$($report.Sections.Licencing.TotalSeats)</strong> &nbsp;·&nbsp; Assigned: <strong>$($report.Sections.Licencing.AssignedSeats)</strong></p>

<h2>Copilot Footprint</h2>
<p>Users with Copilot for Microsoft 365: <strong>$($report.Sections.CopilotFootprint.LicensedUserCount)</strong></p>

<footer>Generated by PAI MSP Toolkit &nbsp;·&nbsp; productionai.institute/msp/discovery &nbsp;·&nbsp; PSF-aligned assessment methodology</footer>
</body></html>
"@
$html | Out-File "$OutputPathdiscovery-summary.html" -Encoding UTF8
Write-Host "  HTML summary saved: $OutputPathdiscovery-summary.html" -ForegroundColor Green
Write-Host ""
Write-Host "Discovery complete. Files in: $OutputPath" -ForegroundColor Cyan
Write-Host "Next step: Open discovery-report.json and discovery-summary.html and review with your team before stakeholder interviews." -ForegroundColor Yellow

After the scripts run

You now have structured data. Here is what to do with it before the client presentation.

1
Review discovery-summary.html

Open the HTML report first. RED findings need to be resolved before any AI deployment. AMBER findings go into the roadmap. Share this internally with your delivery lead before the readout.

2
Map to PSF domains

Use the risk register output to map each finding to the eight PSF domains. This gives your readout structure and connects every finding to a recognised governance framework.

3
Prepare the stakeholder interview guide

The scripts get the technical picture. The interviews get the human one. Use the Assessment Facilitation Guide to run structured conversations with business owners before presenting.

4
Build the roadmap

Sort findings by RAG status and business impact. Red findings = immediate blockers. Amber = 30-day targets. Green = baseline to maintain. Each finding becomes a scoped deliverable.

5
Package the deliverable

The discovery report, risk register, roadmap, and ROI model together form the readout deck. Use the assessment report template from the toolkit and the ROI calculator to complete the package.

6
Transition to delivery

Every finding is a potential engagement. The assessment SOW is complete. The next SOW covers remediation, policy work, or deployment — depending on what the client prioritises.

Accompanying templates

Available to Certified AI Integrators via the partner toolkit.

PS1
run-all-discovery.ps1
Wrapper script that runs scripts 01–08 in sequence with a single command.
XLSX
discovery-output-template.xlsx
Pre-formatted Excel workbook to paste CSV outputs into for client-ready presentation.
DOCX
discovery-report-template.docx
Word template for the written discovery summary report. Maps to the HTML output.
PDF
discovery-checklist.pdf
Print-ready checklist for the on-site discovery day. Sign off each item as you go.

Ready to run your first assessment?

The scripts are one piece. The full assessment methodology — stakeholder interviews, scoring, the readout — is in the Assessment Facilitation Guide. Both are included for Certified AI Integrators.

Assessment facilitation guide →ROI calculator →Become a Certified Integrator →