refactor(scripts): optimize system remediation and M365 export scripts

- teams: harden Classic Teams removal, protect New Teams, and clean HKU run keys
- dotnet: add bootstrapper EXE uninstall support and remove orphaned 3.1.* folders
- teams: add MSIX package targeting and version parsing for vulnerable New Teams builds
- m365: improve Graph SDK export speed, reuse sessions, and add null-safe handling
This commit is contained in:
mattspeer committed 2026-10-04 10:58:05 -05:00
1 parent 4fd95b76d6
commit 5ef82842f2
19 files changed
+1651 -1268

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
<#
.SYNOPSIS
Recursively downloads all SharePoint document libraries and files from a site using Microsoft Graph.
.DESCRIPTION
Connects to the Microsoft Graph API, resolves a SharePoint site URL, enumerates all document libraries (drives),
and recursively downloads folders and files to a designated local target directory.
Includes automatic filename sanitization for Windows file systems, pagination support, skipping existing files,
and a final download statistics report.
.PARAMETER SiteUrl
The full URL of the target SharePoint site (e.g., "https://zefiromethane.sharepoint.com/sites/contentTypeHub").
.PARAMETER Destination
The local root folder path where document libraries and files will be saved.
.PARAMETER SkipAuthentication
Switch parameter to skip 'Connect-MgGraph' if an active Graph session is already established.
.EXAMPLE
.\Export-SharePointSiteFiles.ps1 -SiteUrl "https://zefiromethane.sharepoint.com/sites/contentTypeHub" -Destination "F:\TeamSite"
.EXAMPLE
.\Export-SharePointSiteFiles.ps1 -SiteUrl "https://contoso.sharepoint.com/sites/hr" -Destination "C:\Backup\HR" -Verbose
.NOTES
Created: 2025-03-05
Modified: 2026-10-04
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[ValidateNotNullOrEmpty()]
[string]$SiteUrl,
[Parameter(Mandatory = $true, Position = 1)]
[ValidateNotNullOrEmpty()]
[string]$Destination,
[switch]$SkipAuthentication
)
# 1. Prerequisite Module Check
if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) {
Write-Error "The 'Microsoft.Graph.Authentication' module is required. Install it using: Install-Module Microsoft.Graph"
return
}
# 2. State & Metric Counters
$script:downloadCount = 0
$script:skippedCount = 0
$script:folderCount = 0
$script:errorCount = 0
$script:totalBytes = 0
# 3. Helper Functions
function Sanitize-PathSegment {
param([string]$Name)
# Replaces Windows reserved filename characters: \ / : * ? " < > |
$invalidChars = [System.IO.Path]::GetInvalidFileNameChars()
$sanitized = $Name
foreach ($char in $invalidChars) {
$sanitized = $sanitized.Replace([string]$char, '_')
}
return $sanitized.TrimEnd('.')
}
function Get-GraphCollection {
param([string]$Uri)
do {
$response = Invoke-MgGraphRequest -Method GET -Uri $Uri -ErrorAction Stop
foreach ($item in @($response.value)) {
Write-Output $item
}
$Uri = $response.'@odata.nextLink'
}
while ($Uri)
}
function Copy-GraphFolder {
param(
[string]$DriveId,
[string]$ChildrenUri,
[string]$LocalFolder
)
if (-not (Test-Path -LiteralPath $LocalFolder)) {
New-Item -ItemType Directory -Path $LocalFolder -Force | Out-Null
}
try {
$items = Get-GraphCollection -Uri $ChildrenUri
}
catch {
Write-Error "Failed to retrieve folder contents from URI: $ChildrenUri. Error: $_"
$script:errorCount++
return
}
foreach ($item in $items) {
# Sanitize item name to prevent Windows file path errors
$safeName = Sanitize-PathSegment -Name $item.name
$localPath = Join-Path -Path $LocalFolder -ChildPath $safeName
$escapedDriveId = [Uri]::EscapeDataString($DriveId)
$escapedItemId = [Uri]::EscapeDataString([string]$item.id)
# Handle Folders / Packages
if ($null -ne $item.folder -or $null -ne $item.package) {
$script:folderCount++
New-Item -ItemType Directory -Path $localPath -Force | Out-Null
$childUri = "https://graph.microsoft.com/v1.0/drives/$escapedDriveId/items/$escapedItemId/children"
Copy-GraphFolder -DriveId $DriveId -ChildrenUri $childUri -LocalFolder $localPath
}
# Handle Files
elseif ($null -ne $item.file) {
if (Test-Path -LiteralPath $localPath) {
Write-Verbose "Skipping existing file: $localPath"
$script:skippedCount++
continue
}
Write-Host "Downloading: $safeName" -ForegroundColor Cyan
$contentUri = "https://graph.microsoft.com/v1.0/drives/$escapedDriveId/items/$escapedItemId/content"
try {
Invoke-MgGraphRequest -Method GET -Uri $contentUri -OutputFilePath $localPath -ErrorAction Stop
$script:downloadCount++
if ($item.size) { $script:totalBytes += $item.size }
}
catch {
Write-Error "Failed to download '$($item.name)' to '$localPath'. Error: $_"
$script:errorCount++
}
}
else {
Write-Warning "Unsupported item skipped: $($item.name)"
$script:skippedCount++
}
}
}
# 4. Main Execution Flow
try {
# Establish Graph Connection
if (-not $SkipAuthentication) {
Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Yellow
Connect-MgGraph -Scopes "Sites.Read.All" -NoWelcome
}
# Resolve SharePoint Site ID
$parsedSiteUrl = [Uri]$SiteUrl
$sitePath = $parsedSiteUrl.AbsolutePath.TrimEnd("/")
if ([string]::IsNullOrWhiteSpace($sitePath)) {
$siteLookupUri = "https://graph.microsoft.com/v1.0/sites/$($parsedSiteUrl.Host)"
}
else {
$siteLookupUri = "https://graph.microsoft.com/v1.0/sites/$($parsedSiteUrl.Host):$sitePath"
}
Write-Host "Resolving SharePoint site..." -ForegroundColor Yellow
$site = Invoke-MgGraphRequest -Method GET -Uri $siteLookupUri -ErrorAction Stop
# Ensure root destination folder exists
if (-not (Test-Path -LiteralPath $Destination)) {
New-Item -ItemType Directory -Path $Destination -Force | Out-Null
}
# Enumerate Site Drives (Document Libraries)
$drivesUri = "https://graph.microsoft.com/v1.0/sites/$($site.id)/drives"
$drives = Get-GraphCollection -Uri $drivesUri
foreach ($drive in $drives) {
$safeDriveName = Sanitize-PathSegment -Name $drive.name
$localLibrary = Join-Path -Path $Destination -ChildPath $safeDriveName
$escapedDriveId = [Uri]::EscapeDataString([string]$drive.id)
Write-Host "`n========================================" -ForegroundColor Green
Write-Host "Downloading Library: $($drive.name)" -ForegroundColor Green
Write-Host "Local Target : $localLibrary" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
$rootChildrenUri = "https://graph.microsoft.com/v1.0/drives/$escapedDriveId/root/children"
Copy-GraphFolder -DriveId $drive.id -ChildrenUri $rootChildrenUri -LocalFolder$localLibrary
}
# Display Execution Summary
$MBTransferred = [math]::Round($script:totalBytes / 1MB, 2)
Write-Host "`nSharePoint Download Summary:" -ForegroundColor Green
Write-Host " Files Downloaded : $script:downloadCount ($MBTransferred MB)"
Write-Host " Files Skipped : $script:skippedCount"
Write-Host " Folders Created : $script:folderCount"
Write-Host " Errors Encountered: $script:errorCount"
}
finally {
if (-not $SkipAuthentication) {
Write-Host "`nDisconnecting Microsoft Graph session..." -ForegroundColor Yellow
Disconnect-MgGraph
}
}