diff --git a/DNS Detection Script.ps1 b/DNS Detection Script.ps1 deleted file mode 100755 index 341c307..0000000 --- a/DNS Detection Script.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -''' - -Created: 2025-03-05 -Modified: 2025-03-05 -Description: This script checks to ensure the DNS servers are set to the DNS servers that filter malware - -''' - -# Get all network adapters -$adapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } - -# Check DNS server settings -foreach ($adapter in $adapters) { - $dnsServers = Get-DnsClientServerAddress -InterfaceIndex $adapter.InterfaceIndex - $staticDnsServers = $dnsServers | Where-Object { $_.ServerAddresses -contains '9.9.9.9' -or $_.ServerAddresses -contains '149.112.112.11' -or $_.ServerAddresses -contains '1.1.1.3' -or $_.ServerAddresses -contains '1.0.0.3' } - - if ($staticDnsServers.Count -eq 0) { - Write-Host "Adapter $($adapter.Name) does not have the correct DNS servers configured." - $compliance = "false" - } - else { - Write-Host "Adapter $($adapter.Name) has the correct DNS servers configured." - $compliance = "true" - } -} - -$hash = @{ Compliant = $compliance} -return $hash | ConvertTo-Json -Compress \ No newline at end of file diff --git a/DirectoryListing.ps1 b/DirectoryListing.ps1 deleted file mode 100755 index d8cfee8..0000000 --- a/DirectoryListing.ps1 +++ /dev/null @@ -1,209 +0,0 @@ -$root = 'E:\Sync Method\Corporate\Zefiro Methane Corp. - Documents' -$outputDirectory = 'C:\Users\MattSpeer\OneDrive - Add Source Group\Documents\1- Projects\Zefiro - Data Migration' -$textFile = Join-Path $outputDirectory 'Zefiro-Methane-Corp-Documents-tree.txt' -$htmlFile = Join-Path $outputDirectory 'Zefiro-Methane-Corp-Documents-tree.html' - -if (-not (Test-Path -LiteralPath $root -PathType Container)) { - throw "Folder not found: $root" -} - -New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null - -$script:textLines = [System.Collections.Generic.List[string]]::new() -$script:html = [System.Text.StringBuilder]::new() -$script:scanErrors = 0 - -function ConvertTo-HtmlEncoded { - param([string]$Text) - return [System.Net.WebUtility]::HtmlEncode($Text) -} - -function Add-TreeItems { - param( - [string]$Path, - [string]$Prefix - ) - - try { - $items = @( - Get-ChildItem -LiteralPath $Path -Force -ErrorAction Stop | - Sort-Object @{ Expression = { if ($_.PSIsContainer) { 0 } else { 1 } } }, Name - ) - } - catch { - $script:scanErrors++ - [void]$script:textLines.Add("$Prefix[Unable to read: $($_.Exception.Message)]") - [void]$script:html.AppendLine( - "
  • Unable to read: $(ConvertTo-HtmlEncoded $_.Exception.Message)
  • " - ) - return - } - - for ($index = 0; $index -lt $items.Count; $index++) { - $item = $items[$index] - $isLast = $index -eq ($items.Count - 1) - $connector = if ($isLast) { '\-- ' } else { '|-- ' } - $nextPrefix = $Prefix + $(if ($isLast) { ' ' } else { '| ' }) - - $nameHtml = ConvertTo-HtmlEncoded $item.Name - $pathHtml = ConvertTo-HtmlEncoded $item.FullName - $isLink = -not [string]::IsNullOrWhiteSpace([string]$item.LinkType) - - if ($item.PSIsContainer -and -not $isLink) { - [void]$script:textLines.Add("$Prefix$connector$($item.Name)") - [void]$script:html.AppendLine( - "
  • 📁 $nameHtml
  • ') - } - elseif ($item.PSIsContainer -and $isLink) { - $target = @($item.Target) -join ', ' - [void]$script:textLines.Add( - "$Prefix$connector$($item.Name) [Link → $target]" - ) - [void]$script:html.AppendLine( - "" - ) - } - else { - [void]$script:textLines.Add("$Prefix$connector$($item.Name)") - [void]$script:html.AppendLine( - "
  • 📄 $nameHtml
  • " - ) - } - } -} - -$rootNameHtml = ConvertTo-HtmlEncoded (Split-Path -Path $root -Leaf) -$rootPathHtml = ConvertTo-HtmlEncoded $root -$generated = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' - -[void]$script:textLines.Add($root) - -[void]$script:html.AppendLine(@" - - - - - -Team Site Documents Directory - - - -

    Team Site – Documents

    -
    -Generated $generated
    -Root: $rootPathHtml -
    -
    - - - -
    - - - - -"@) - -$script:textLines | Set-Content -LiteralPath $textFile -Encoding UTF8 -$script:html.ToString() | Set-Content -LiteralPath $htmlFile -Encoding UTF8 - -Write-Host "Text report: $textFile" -Write-Host "HTML report: $htmlFile" -Write-Host "Unreadable folders: $script:scanErrors" - -Start-Process $htmlFile \ No newline at end of file diff --git a/Download-All-SharePoint-Files.ps1 b/Download-All-SharePoint-Files.ps1 deleted file mode 100755 index 0305705..0000000 --- a/Download-All-SharePoint-Files.ps1 +++ /dev/null @@ -1,112 +0,0 @@ -$SiteUrl = "https://zefiromethane.sharepoint.com/sites/contentTypeHub" -$Destination = "F:\TeamSite" - -# Opens a Microsoft sign-in window. -Connect-MgGraph -Scopes "Sites.Read.All" -NoWelcome - -function Get-GraphCollection { - param([string]$Uri) - - do { - $response = Invoke-MgGraphRequest -Method GET -Uri $Uri - - foreach ($item in @($response.value)) { - Write-Output $item - } - - $Uri = $response.'@odata.nextLink' - } - while ($Uri) -} - -function Copy-GraphFolder { - param( - [string]$DriveId, - [string]$ChildrenUri, - [string]$LocalFolder - ) - - New-Item -ItemType Directory -Path $LocalFolder -Force | - Out-Null - - foreach ($item in Get-GraphCollection -Uri $ChildrenUri) { - $localPath = Join-Path $LocalFolder $item.name - $escapedDriveId = [Uri]::EscapeDataString($DriveId) - $escapedItemId = [Uri]::EscapeDataString([string]$item.id) - - if ($null -ne $item.folder -or $null -ne $item.package) { - # Create the folder, including empty folders. - 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 - } - elseif ($null -ne $item.file) { - if (Test-Path -LiteralPath $localPath) { - Write-Host "Skipping existing file: $localPath" - continue - } - - Write-Host "Downloading: $localPath" - - $contentUri = ` - "https://graph.microsoft.com/v1.0/drives/" + - "$escapedDriveId/items/$escapedItemId/content" - - Invoke-MgGraphRequest ` - -Method GET ` - -Uri $contentUri ` - -OutputFilePath $localPath - } - else { - Write-Warning "Unsupported item skipped: $($item.name)" - } - } -} - -# Resolve the SharePoint site from its URL. -$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" -} - -$site = Invoke-MgGraphRequest -Method GET -Uri $siteLookupUri - -New-Item -ItemType Directory -Path $Destination -Force | - Out-Null - -# A Graph "drive" represents a SharePoint document library. -$drivesUri = "https://graph.microsoft.com/v1.0/sites/$($site.id)/drives" - -foreach ($drive in Get-GraphCollection -Uri $drivesUri) { - $localLibrary = Join-Path $Destination $drive.name - $escapedDriveId = [Uri]::EscapeDataString([string]$drive.id) - - Write-Host "Downloading library: $($drive.name)" - - $rootChildrenUri = ` - "https://graph.microsoft.com/v1.0/drives/" + - "$escapedDriveId/root/children" - - Copy-GraphFolder ` - -DriveId $drive.id ` - -ChildrenUri $rootChildrenUri ` - -LocalFolder $localLibrary -} - -Disconnect-MgGraph \ No newline at end of file diff --git a/Export-DocuSignCompletedAgreements.ps1 b/Export-DocuSignCompletedAgreements.ps1 deleted file mode 100644 index 5abb328..0000000 --- a/Export-DocuSignCompletedAgreements.ps1 +++ /dev/null @@ -1,378 +0,0 @@ -<# -.SYNOPSIS -Exports completed DocuSign envelopes, including the Certificate of Completion. - -.DESCRIPTION -Uses the DocuSign eSignature REST API to: - 1. Authenticate with JWT. - 2. List completed envelopes in a date range. - 3. Download each envelope as either an archive ZIP or combined PDF. - 4. Write a CSV export log. - -Prerequisites: - - PowerShell 7 recommended. - - A DocuSign app/integration key. - - RSA private key PEM for the integration. - - Consent granted for scopes: signature impersonation. - - The impersonated user must have access to the target envelopes. - -Consent URL format: - Production: - https://account.docusign.com/oauth/auth?response_type=code&scope=signature%20impersonation&client_id=INTEGRATION_KEY&redirect_uri=https://www.docusign.com - - Demo: - https://account-d.docusign.com/oauth/auth?response_type=code&scope=signature%20impersonation&client_id=INTEGRATION_KEY&redirect_uri=https://www.docusign.com - -.EXAMPLE -.\Export-DocuSignCompletedAgreements.ps1 ` - -IntegrationKey "00000000-0000-0000-0000-000000000000" ` - -UserId "11111111-1111-1111-1111-111111111111" ` - -AccountId "22222222-2222-2222-2222-222222222222" ` - -PrivateKeyPath "C:\Keys\docusign_private.key" ` - -FromDate "2020-01-01" ` - -OutputRoot "D:\DocuSignExport" ` - -OutputFormat ArchiveZip -#> - -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)] - [string]$IntegrationKey, - - [Parameter(Mandatory = $true)] - [string]$UserId, - - [Parameter(Mandatory = $true)] - [string]$AccountId, - - [Parameter(Mandatory = $true)] - [string]$PrivateKeyPath, - - [Parameter(Mandatory = $true)] - [datetime]$FromDate, - - [Parameter(Mandatory = $false)] - [datetime]$ToDate = (Get-Date), - - [Parameter(Mandatory = $true)] - [string]$OutputRoot, - - [Parameter(Mandatory = $false)] - [ValidateSet("Production", "Demo")] - [string]$Environment = "Production", - - [Parameter(Mandatory = $false)] - [ValidateSet("ArchiveZip", "CombinedPdf")] - [string]$OutputFormat = "ArchiveZip", - - [Parameter(Mandatory = $false)] - [int]$PageSize = 1000, - - [Parameter(Mandatory = $false)] - [int]$RetryCount = 4 -) - -$ErrorActionPreference = "Stop" - -function ConvertTo-Base64Url { - param([byte[]]$Bytes) - [Convert]::ToBase64String($Bytes).TrimEnd("=").Replace("+", "-").Replace("/", "_") -} - -function ConvertFrom-Base64UrlString { - param([string]$Value) - $base64 = $Value.Replace("-", "+").Replace("_", "/") - switch ($base64.Length % 4) { - 2 { $base64 += "==" } - 3 { $base64 += "=" } - } - [Convert]::FromBase64String($base64) -} - -function Import-RsaPrivateKey { - param([string]$Path) - - $pem = Get-Content -Path $Path -Raw - $rsa = [System.Security.Cryptography.RSA]::Create() - - if ($pem -match "-----BEGIN RSA PRIVATE KEY-----") { - $body = $pem -replace "-----BEGIN RSA PRIVATE KEY-----", "" -replace "-----END RSA PRIVATE KEY-----", "" -replace "\s", "" - $bytes = [Convert]::FromBase64String($body) - [void]$rsa.ImportRSAPrivateKey($bytes, [ref]0) - return $rsa - } - - if ($pem -match "-----BEGIN PRIVATE KEY-----") { - $body = $pem -replace "-----BEGIN PRIVATE KEY-----", "" -replace "-----END PRIVATE KEY-----", "" -replace "\s", "" - $bytes = [Convert]::FromBase64String($body) - [void]$rsa.ImportPkcs8PrivateKey($bytes, [ref]0) - return $rsa - } - - throw "Private key must be PEM formatted with BEGIN RSA PRIVATE KEY or BEGIN PRIVATE KEY." -} - -function New-DocuSignJwtAssertion { - param( - [string]$IntegrationKey, - [string]$UserId, - [string]$AuthServer, - [string]$PrivateKeyPath - ) - - $now = [DateTimeOffset]::UtcNow - $header = @{ - alg = "RS256" - typ = "JWT" - } | ConvertTo-Json -Compress - - $payload = @{ - iss = $IntegrationKey - sub = $UserId - aud = $AuthServer - iat = $now.ToUnixTimeSeconds() - exp = $now.AddMinutes(55).ToUnixTimeSeconds() - scope = "signature impersonation" - } | ConvertTo-Json -Compress - - $encodedHeader = ConvertTo-Base64Url ([Text.Encoding]::UTF8.GetBytes($header)) - $encodedPayload = ConvertTo-Base64Url ([Text.Encoding]::UTF8.GetBytes($payload)) - $unsignedToken = "$encodedHeader.$encodedPayload" - - $rsa = Import-RsaPrivateKey -Path $PrivateKeyPath - try { - $signature = $rsa.SignData( - [Text.Encoding]::UTF8.GetBytes($unsignedToken), - [Security.Cryptography.HashAlgorithmName]::SHA256, - [Security.Cryptography.RSASignaturePadding]::Pkcs1 - ) - } - finally { - $rsa.Dispose() - } - - "$unsignedToken.$(ConvertTo-Base64Url $signature)" -} - -function Invoke-WithRetry { - param( - [scriptblock]$ScriptBlock, - [int]$RetryCount = 4, - [string]$ActionName = "DocuSign API request" - ) - - for ($attempt = 1; $attempt -le ($RetryCount + 1); $attempt++) { - try { - return & $ScriptBlock - } - catch { - $response = $_.Exception.Response - $statusCode = $null - if ($response -and $response.StatusCode) { - $statusCode = [int]$response.StatusCode - } - - $retryable = $statusCode -in @(429, 500, 502, 503, 504) - if (-not $retryable -or $attempt -gt $RetryCount) { - throw - } - - $delaySeconds = [math]::Min(60, [math]::Pow(2, $attempt)) - Write-Warning "$ActionName failed with HTTP $statusCode. Retrying in $delaySeconds seconds. Attempt $attempt of $RetryCount." - Start-Sleep -Seconds $delaySeconds - } - } -} - -function Get-SafeFileName { - param( - [string]$Text, - [int]$MaxLength = 120 - ) - - $invalid = [Regex]::Escape(([IO.Path]::GetInvalidFileNameChars() -join "")) - $safe = [Regex]::Replace($Text, "[$invalid]", "_") - $safe = [Regex]::Replace($safe, "\s+", " ").Trim() - if ([string]::IsNullOrWhiteSpace($safe)) { - $safe = "Envelope" - } - if ($safe.Length -gt $MaxLength) { - $safe = $safe.Substring(0, $MaxLength).Trim() - } - $safe -} - -function Save-Bytes { - param( - [string]$Uri, - [hashtable]$Headers, - [string]$Path, - [int]$RetryCount - ) - - Invoke-WithRetry -RetryCount $RetryCount -ActionName "Download $Path" -ScriptBlock { - Invoke-WebRequest -Uri $Uri -Headers $Headers -OutFile $Path - } | Out-Null -} - -$authServer = if ($Environment -eq "Demo") { "account-d.docusign.com" } else { "account.docusign.com" } -$tokenUri = "https://$authServer/oauth/token" - -Write-Host "Requesting DocuSign access token..." -$assertion = New-DocuSignJwtAssertion ` - -IntegrationKey $IntegrationKey ` - -UserId $UserId ` - -AuthServer $authServer ` - -PrivateKeyPath $PrivateKeyPath - -$tokenResponse = Invoke-WithRetry -RetryCount $RetryCount -ActionName "JWT token request" -ScriptBlock { - Invoke-RestMethod ` - -Method Post ` - -Uri $tokenUri ` - -ContentType "application/x-www-form-urlencoded" ` - -Body @{ - grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer" - assertion = $assertion - } -} - -$headers = @{ - Authorization = "Bearer $($tokenResponse.access_token)" -} - -Write-Host "Discovering account base URI..." -$userInfo = Invoke-WithRetry -RetryCount $RetryCount -ActionName "UserInfo request" -ScriptBlock { - Invoke-RestMethod -Method Get -Uri "https://$authServer/oauth/userinfo" -Headers $headers -} - -$account = $userInfo.accounts | Where-Object { $_.account_id -eq $AccountId } | Select-Object -First 1 -if (-not $account) { - throw "AccountId '$AccountId' was not found for the impersonated user." -} - -$baseUri = $account.base_uri.TrimEnd("/") -$apiBase = "$baseUri/restapi/v2.1/accounts/$AccountId" - -$downloadFolder = Join-Path $OutputRoot "completed-agreements" -$logFolder = Join-Path $OutputRoot "logs" -New-Item -ItemType Directory -Path $downloadFolder -Force | Out-Null -New-Item -ItemType Directory -Path $logFolder -Force | Out-Null - -$timestamp = Get-Date -Format "yyyyMMdd-HHmmss" -$logPath = Join-Path $logFolder "docusign-export-log-$timestamp.csv" - -$documentId = if ($OutputFormat -eq "ArchiveZip") { "archive" } else { "combined" } -$extension = if ($OutputFormat -eq "ArchiveZip") { ".zip" } else { ".pdf" } -$accept = if ($OutputFormat -eq "ArchiveZip") { "application/zip" } else { "application/pdf" } - -$downloadHeaders = @{ - Authorization = "Bearer $($tokenResponse.access_token)" - Accept = $accept -} - -$from = $FromDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") -$to = $ToDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") - -Write-Host "Finding completed envelopes from $from to $to..." -$allEnvelopes = New-Object System.Collections.Generic.List[object] -$startPosition = 0 - -do { - $query = @{ - from_date = $from - to_date = $to - status = "completed" - count = $PageSize - start_position = $startPosition - order_by = "completed" - order = "asc" - } - - $queryString = ($query.GetEnumerator() | ForEach-Object { - "{0}={1}" -f [Uri]::EscapeDataString($_.Key), [Uri]::EscapeDataString([string]$_.Value) - }) -join "&" - - $uri = "$apiBase/envelopes?$queryString" - $page = Invoke-WithRetry -RetryCount $RetryCount -ActionName "Envelope list page starting $startPosition" -ScriptBlock { - Invoke-RestMethod -Method Get -Uri $uri -Headers $headers - } - - if ($page.envelopes) { - foreach ($envelope in $page.envelopes) { - $allEnvelopes.Add($envelope) - } - } - - $returned = if ($page.envelopes) { @($page.envelopes).Count } else { 0 } - Write-Host "Found $returned envelopes on page starting at $startPosition." - - $startPosition += $PageSize - $totalSetSize = [int]$page.totalSetSize -} while ($allEnvelopes.Count -lt $totalSetSize -and $returned -gt 0) - -Write-Host "Total completed envelopes found: $($allEnvelopes.Count)" - -$logRows = New-Object System.Collections.Generic.List[object] -$counter = 0 - -foreach ($envelope in $allEnvelopes) { - $counter++ - $envelopeId = $envelope.envelopeId - $subject = Get-SafeFileName -Text $envelope.emailSubject - - $completedRaw = if ($envelope.completedDateTime) { $envelope.completedDateTime } else { $envelope.statusChangedDateTime } - $completedDate = if ($completedRaw) { - ([datetime]$completedRaw).ToString("yyyy-MM-dd") - } else { - "unknown-date" - } - - $fileName = "$completedDate - $subject - $envelopeId$extension" - $filePath = Join-Path $downloadFolder $fileName - $downloadUri = "$apiBase/envelopes/$envelopeId/documents/$documentId`?certificate=true" - - Write-Progress ` - -Activity "Downloading completed DocuSign agreements" ` - -Status "$counter of $($allEnvelopes.Count): $subject" ` - -PercentComplete (($counter / [math]::Max(1, $allEnvelopes.Count)) * 100) - - $status = "Downloaded" - $errorMessage = "" - - try { - Save-Bytes -Uri $downloadUri -Headers $downloadHeaders -Path $filePath -RetryCount $RetryCount - } - catch { - $status = "Failed" - $errorMessage = $_.Exception.Message - Write-Warning "Failed to download envelope $envelopeId`: $errorMessage" - } - - $logRows.Add([PSCustomObject]@{ - EnvelopeId = $envelopeId - Status = $status - EnvelopeStatus = $envelope.status - CompletedDate = $completedRaw - Subject = $envelope.emailSubject - OutputFormat = $OutputFormat - OutputPath = $filePath - Error = $errorMessage - }) -} - -$logRows | Export-Csv -Path $logPath -NoTypeInformation -Encoding UTF8 - -$downloaded = @($logRows | Where-Object { $_.Status -eq "Downloaded" }).Count -$failed = @($logRows | Where-Object { $_.Status -eq "Failed" }).Count - -Write-Host "" -Write-Host "DocuSign export complete." -Write-Host "Completed envelopes found: $($allEnvelopes.Count)" -Write-Host "Downloaded successfully: $downloaded" -Write-Host "Failed: $failed" -Write-Host "Files saved to: $downloadFolder" -Write-Host "Log saved to: $logPath" - -if ($failed -gt 0) { - Write-Warning "Some envelopes failed. Review the CSV log and rerun after resolving the errors." -} diff --git a/MDM-Enrollment.ps1 b/MDM-Enrollment.ps1 deleted file mode 100755 index 4b9557d..0000000 --- a/MDM-Enrollment.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -#!powershell - -''' - -Created: 2025-01-13 -Modified: 2025-01-15 -Description: This script will enroll the device into MDM. - -''' - -Write-Host -ForegroundColor Red "WARNING: This script will enroll the current device into MDM. This is meant for Add Source Group owned devices only. Do you want to continue? (Y/N)" -$answer = Read-Host - -if ($answer -ne "Y") { - Write-Output "Exiting script - Device will not be enrolled into MDM" - exit -} - -Write-Output "Enrolling device into MDM" - -Start-Process -FilePath 'C:\Windows\system32\deviceenroller.exe' -ArgumentList '/c /AutoEnrollMDM' - -Write-Host -ForegroundColor Green "Device enrolled script has completed. You can now close this window." \ No newline at end of file diff --git a/Remove-ClassicTeams.ps1 b/Remove-ClassicTeams.ps1 deleted file mode 100644 index abfd793..0000000 --- a/Remove-ClassicTeams.ps1 +++ /dev/null @@ -1,131 +0,0 @@ -# Remove-ClassicTeams.ps1 -# Removes classic Microsoft Teams and Teams Machine-Wide Installer. -# Leaves new Teams / MSTeams MSIX app installed. - -$ErrorActionPreference = "Continue" - -Write-Output "Starting classic Teams cleanup..." - -# Stop Teams-related processes -$processNames = @( - "Teams", - "ms-teams", - "msteams", - "Microsoft.Teams", - "Update" -) - -foreach ($process in $processNames) { - Get-Process -Name $process -ErrorAction SilentlyContinue | - Stop-Process -Force -ErrorAction SilentlyContinue -} - -Start-Sleep -Seconds 5 - -# Remove Teams Machine-Wide Installer from HKLM uninstall registry -$uninstallPaths = @( - "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", - "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" -) - -$machineWideInstallers = foreach ($path in $uninstallPaths) { - Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { - $_.DisplayName -eq "Teams Machine-Wide Installer" - } -} - -foreach ($app in $machineWideInstallers) { - Write-Output "Found Teams Machine-Wide Installer: $($app.DisplayName) $($app.DisplayVersion)" - - if ($app.UninstallString -match "\{[A-Fa-f0-9\-]{36}\}") { - $productCode = $Matches[0] - Write-Output "Removing Teams Machine-Wide Installer using MSI product code: $productCode" - Start-Process -FilePath "msiexec.exe" -ArgumentList "/x $productCode /qn /norestart" -Wait - } - elseif ($app.UninstallString) { - Write-Output "Attempting uninstall using uninstall string: $($app.UninstallString)" - Start-Process -FilePath "cmd.exe" -ArgumentList "/c $($app.UninstallString) /quiet /norestart" -Wait - } -} - -# Remove classic Teams per-user installs -$userProfiles = Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue | Where-Object { - $_.Name -notin @( - "Public", - "Default", - "Default User", - "All Users", - "defaultuser0" - ) -} - -foreach ($profile in $userProfiles) { - $teamsFolder = Join-Path $profile.FullName "AppData\Local\Microsoft\Teams" - $updateExe = Join-Path $teamsFolder "Update.exe" - - if (Test-Path $updateExe) { - Write-Output "Uninstalling classic Teams for profile: $($profile.Name)" - Start-Process -FilePath $updateExe -ArgumentList "--uninstall -s" -Wait -ErrorAction SilentlyContinue - Start-Sleep -Seconds 3 - } - - if (Test-Path $teamsFolder) { - Write-Output "Removing remaining classic Teams folder for profile: $($profile.Name)" - Remove-Item -Path $teamsFolder -Recurse -Force -ErrorAction SilentlyContinue - } -} - -# Remove common classic Teams leftovers -$leftoverPaths = @( - "$env:ProgramData\Microsoft\Teams", - "$env:ProgramData\Microsoft\TeamsMachineInstaller" -) - -foreach ($path in $leftoverPaths) { - if (Test-Path $path) { - Write-Output "Removing leftover path: $path" - Remove-Item -Path $path -Recurse -Force -ErrorAction SilentlyContinue - } -} - -# Confirm new Teams remains installed -$newTeams = Get-AppxPackage -AllUsers | Where-Object { - $_.Name -eq "MSTeams" -} - -if ($newTeams) { - Write-Output "New Teams detected and left installed:" - $newTeams | Select-Object Name, Version, PackageFullName | Format-List | Out-String | Write-Output -} -else { - Write-Output "WARNING: New Teams / MSTeams was not detected after cleanup." -} - -# Final verification for classic Teams entries -$remainingMachineWide = foreach ($path in $uninstallPaths) { - Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { - $_.DisplayName -eq "Teams Machine-Wide Installer" - } -} - -$remainingClassicFolders = foreach ($profile in $userProfiles) { - $teamsFolder = Join-Path $profile.FullName "AppData\Local\Microsoft\Teams" - if (Test-Path $teamsFolder) { - $teamsFolder - } -} - -if ($remainingMachineWide -or $remainingClassicFolders) { - Write-Output "Classic Teams cleanup completed, but some remnants may remain." - if ($remainingMachineWide) { - $remainingMachineWide | Select-Object DisplayName, DisplayVersion | Format-Table | Out-String | Write-Output - } - if ($remainingClassicFolders) { - $remainingClassicFolders | ForEach-Object { Write-Output "Remaining folder: $_" } - } - exit 1 -} -else { - Write-Output "Classic Teams and Teams Machine-Wide Installer cleanup completed successfully." - exit 0 -} \ No newline at end of file diff --git a/Remove-DotNetCore31.ps1 b/Remove-DotNetCore31.ps1 deleted file mode 100644 index b7a87ef..0000000 --- a/Remove-DotNetCore31.ps1 +++ /dev/null @@ -1,71 +0,0 @@ -# Remove-DotNetCore31.ps1 -# Removes unsupported Microsoft .NET Core / ASP.NET Core / Windows Desktop Runtime / SDK 3.1.x components - -$ErrorActionPreference = "Continue" - -Write-Output "Starting .NET Core 3.1 remediation..." - -$registryPaths = @( - "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", - "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" -) - -$targetApps = foreach ($path in $registryPaths) { - Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { - ( - $_.DisplayName -match "Microsoft \.NET.*3\.1\." -or - $_.DisplayName -match "Microsoft ASP\.NET Core.*3\.1\." -or - $_.DisplayName -match "Microsoft Windows Desktop Runtime.*3\.1\." - ) -and - $_.UninstallString - } -} - -if (-not $targetApps) { - Write-Output "No .NET Core 3.1 components found. Nothing to remove." - exit 0 -} - -foreach ($app in $targetApps) { - Write-Output "Found: $($app.DisplayName) $($app.DisplayVersion)" -} - -foreach ($app in $targetApps) { - Write-Output "Attempting to remove: $($app.DisplayName) $($app.DisplayVersion)" - - $uninstallString = $app.UninstallString - - if ($uninstallString -match "\{[A-Fa-f0-9\-]{36}\}") { - $productCode = $Matches[0] - Write-Output "Using MSI product code: $productCode" - Start-Process -FilePath "msiexec.exe" -ArgumentList "/x $productCode /qn /norestart" -Wait - } - else { - Write-Output "No MSI product code found for $($app.DisplayName). Skipping." - } -} - -Write-Output "Verifying remaining .NET Core 3.1 components..." - -$remainingApps = foreach ($path in $registryPaths) { - Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { - ( - $_.DisplayName -match "Microsoft \.NET.*3\.1\." -or - $_.DisplayName -match "Microsoft ASP\.NET Core.*3\.1\." -or - $_.DisplayName -match "Microsoft Windows Desktop Runtime.*3\.1\." - ) -and - $_.UninstallString - } -} - -if ($remainingApps) { - Write-Output "Some .NET Core 3.1 components may still remain:" - $remainingApps | ForEach-Object { - Write-Output "$($_.DisplayName) $($_.DisplayVersion)" - } - exit 1 -} -else { - Write-Output ".NET Core 3.1 components removed successfully." - exit 0 -} \ No newline at end of file diff --git a/Remove-SpecificTeamsVersions.ps1 b/Remove-SpecificTeamsVersions.ps1 deleted file mode 100644 index d8f13fa..0000000 --- a/Remove-SpecificTeamsVersions.ps1 +++ /dev/null @@ -1,233 +0,0 @@ -# Remove-SpecificTeamsVersions.ps1 -# Removes specified vulnerable/old versions of classic Microsoft Teams and Teams Machine-Wide Installer. -# Leaves new Teams / MSTeams MSIX app installed. - -$ErrorActionPreference = "Continue" - -Write-Output "Starting targeted Microsoft Teams cleanup..." - -# Versions to remove -$TargetVersions = @( - "24334.1103.3302.5694", - "25045.406.3471.2143", - "24124.2402.2858.5617", - "24004.1403.2634.2418", - "23195.1506.2253.6778", - "24277.3102.3183.2670", - "24256.2502.3123.1", - "24165.1306.2986.9504", - "25031.702.3408.1909", - "24244.507.3118.4732", - "23119.303.2080.2726", - "1.7.0.7956", - "25044.2208.3471.2155", - "24348.802.3311.5092", - "23335.242.2641.4129" -) - -# Stop Teams-related processes -$ProcessNames = @( - "Teams", - "ms-teams", - "msteams", - "Microsoft.Teams", - "Update" -) - -foreach ($ProcessName in $ProcessNames) { - Get-Process -Name $ProcessName -ErrorAction SilentlyContinue | - Stop-Process -Force -ErrorAction SilentlyContinue -} - -Start-Sleep -Seconds 5 - -# Registry uninstall locations -$UninstallRegistryPaths = @( - "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", - "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" -) - -# Remove matching Teams Machine-Wide Installer versions -Write-Output "Checking for targeted Teams Machine-Wide Installer versions..." - -$MachineWideInstallers = foreach ($Path in $UninstallRegistryPaths) { - Get-ItemProperty $Path -ErrorAction SilentlyContinue | Where-Object { - $_.DisplayName -eq "Teams Machine-Wide Installer" -and - $TargetVersions -contains $_.DisplayVersion - } -} - -foreach ($App in $MachineWideInstallers) { - Write-Output "Found targeted Teams Machine-Wide Installer: $($App.DisplayVersion)" - - if ($App.UninstallString -match "\{[A-Fa-f0-9\-]{36}\}") { - $ProductCode = $Matches[0] - Write-Output "Removing Teams Machine-Wide Installer using MSI product code: $ProductCode" - - Start-Process -FilePath "msiexec.exe" ` - -ArgumentList "/x $ProductCode /qn /norestart" ` - -Wait - } - elseif ($App.UninstallString) { - Write-Output "Removing Teams Machine-Wide Installer using uninstall string." - - Start-Process -FilePath "cmd.exe" ` - -ArgumentList "/c $($App.UninstallString) /quiet /norestart" ` - -Wait - } - else { - Write-Output "No uninstall string found for Teams Machine-Wide Installer version $($App.DisplayVersion)." - } -} - -# Remove matching classic Teams per-user installs -Write-Output "Checking user profiles for targeted classic Teams versions..." - -$UserProfiles = Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue | Where-Object { - $_.Name -notin @( - "Public", - "Default", - "Default User", - "All Users", - "defaultuser0" - ) -} - -foreach ($Profile in $UserProfiles) { - $TeamsFolder = Join-Path $Profile.FullName "AppData\Local\Microsoft\Teams" - $UpdateExe = Join-Path $TeamsFolder "Update.exe" - $CurrentExe = Join-Path $TeamsFolder "current\Teams.exe" - - if (-not (Test-Path $TeamsFolder)) { - continue - } - - $DetectedVersion = $null - - if (Test-Path $CurrentExe) { - $DetectedVersion = (Get-Item $CurrentExe).VersionInfo.FileVersion - } - - if (-not $DetectedVersion -and (Test-Path $UpdateExe)) { - $DetectedVersion = (Get-Item $UpdateExe).VersionInfo.FileVersion - } - - if ($TargetVersions -contains $DetectedVersion) { - Write-Output "Found targeted classic Teams version $DetectedVersion in profile: $($Profile.Name)" - - if (Test-Path $UpdateExe) { - Write-Output "Attempting silent uninstall for profile: $($Profile.Name)" - Start-Process -FilePath $UpdateExe ` - -ArgumentList "--uninstall -s" ` - -Wait ` - -ErrorAction SilentlyContinue - - Start-Sleep -Seconds 3 - } - - if (Test-Path $TeamsFolder) { - Write-Output "Removing remaining classic Teams folder for profile: $($Profile.Name)" - Remove-Item -Path $TeamsFolder -Recurse -Force -ErrorAction SilentlyContinue - } - } - else { - Write-Output "Skipping profile $($Profile.Name). Classic Teams version '$DetectedVersion' is not in target list." - } -} - -# Check HKCU-style per-user uninstall entries under user registry hives if loaded -Write-Output "Checking loaded user registry hives for targeted classic Teams uninstall entries..." - -$LoadedUserUninstallPaths = Get-ChildItem "Registry::HKEY_USERS" -ErrorAction SilentlyContinue | ForEach-Object { - "Registry::$($_.Name)\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" -} - -foreach ($Path in $LoadedUserUninstallPaths) { - $UserTeamsApps = Get-ItemProperty $Path -ErrorAction SilentlyContinue | Where-Object { - ( - $_.DisplayName -eq "Microsoft Teams" -or - $_.DisplayName -eq "MicrosoftTeams" - ) -and - $TargetVersions -contains $_.DisplayVersion - } - - foreach ($App in $UserTeamsApps) { - Write-Output "Found targeted per-user Teams uninstall entry: $($App.DisplayName) $($App.DisplayVersion)" - - if ($App.UninstallString) { - Start-Process -FilePath "cmd.exe" ` - -ArgumentList "/c $($App.UninstallString) --uninstall -s" ` - -Wait ` - -ErrorAction SilentlyContinue - } - } -} - -# Verify new Teams remains installed -Write-Output "Checking for new Teams / MSTeams..." - -$NewTeams = Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue | Where-Object { - $_.Name -eq "MSTeams" -} - -if ($NewTeams) { - Write-Output "New Teams / MSTeams is installed and was left in place:" - $NewTeams | Select-Object Name, Version, PackageFullName | Format-List | Out-String | Write-Output -} -else { - Write-Output "WARNING: New Teams / MSTeams was not detected." -} - -# Final verification -Write-Output "Performing final verification..." - -$RemainingMachineWide = foreach ($Path in $UninstallRegistryPaths) { - Get-ItemProperty $Path -ErrorAction SilentlyContinue | Where-Object { - $_.DisplayName -eq "Teams Machine-Wide Installer" -and - $TargetVersions -contains $_.DisplayVersion - } -} - -$RemainingClassicTeamsFolders = foreach ($Profile in $UserProfiles) { - $TeamsFolder = Join-Path $Profile.FullName "AppData\Local\Microsoft\Teams" - $CurrentExe = Join-Path $TeamsFolder "current\Teams.exe" - - if (Test-Path $CurrentExe) { - $Version = (Get-Item $CurrentExe).VersionInfo.FileVersion - - if ($TargetVersions -contains $Version) { - [PSCustomObject]@{ - Profile = $Profile.Name - Version = $Version - Path = $TeamsFolder - } - } - } -} - -if ($RemainingMachineWide -or $RemainingClassicTeamsFolders) { - Write-Output "Targeted Teams cleanup completed, but some targeted versions may still remain." - - if ($RemainingMachineWide) { - Write-Output "Remaining Teams Machine-Wide Installer entries:" - $RemainingMachineWide | - Select-Object DisplayName, DisplayVersion, UninstallString | - Format-Table -AutoSize | - Out-String | - Write-Output - } - - if ($RemainingClassicTeamsFolders) { - Write-Output "Remaining classic Teams folders:" - $RemainingClassicTeamsFolders | - Format-Table -AutoSize | - Out-String | - Write-Output - } - - exit 1 -} -else { - Write-Output "Targeted Teams versions were successfully removed or were not present." - exit 0 -} \ No newline at end of file diff --git a/Sharepoint-List-Export.ps1 b/Sharepoint-List-Export.ps1 deleted file mode 100755 index ce406c6..0000000 --- a/Sharepoint-List-Export.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -Install-Module Microsoft.Graph.Authentication -Scope CurrentUser - -# --------------------------- - -& "Test-SharePointDownload.ps1" ` - -SiteUrl "https://zefiromethane.sharepoint.com/sites/zefirocorp" ` - -LibraryName "Documents" ` - -LocalRoot "F:\Zefiro Methane" ` - -OutputDirectory "F:\SharePointValidation" \ No newline at end of file diff --git a/User-Recertification.ps1 b/User-Recertification.ps1 deleted file mode 100644 index bb8e759..0000000 --- a/User-Recertification.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -# Export-M365UserCertification.ps1 -# Requires Microsoft Graph PowerShell SDK -# -# To run: .\User-Recertification.ps1 -OutputPath "C:\Users\Matt\OneDrive - Add Source Group\Documents\1- Projects\M365-User-Certification-Export.csv" - -param( - [string]$OutputPath = ".\M365-User-Certification-Export.csv" -) - -# Install if needed: -# Install-Module Microsoft.Graph -Scope CurrentUser - -Import-Module Microsoft.Graph.Users -Import-Module Microsoft.Graph.Identity.DirectoryManagement - -$Scopes = @( - "User.Read.All", - "Directory.Read.All", - "AuditLog.Read.All" -) - -Connect-MgGraph -Scopes $Scopes - -# Build SKU lookup so license GUIDs become readable names -$SkuLookup = @{} -Get-MgSubscribedSku -All | ForEach-Object { - $SkuLookup[$_.SkuId.ToString()] = $_.SkuPartNumber -} - -$Properties = @( - "id", - "displayName", - "userPrincipalName", - "userType", - "accountEnabled", - "assignedLicenses", - "externalUserState", - "signInActivity" -) - -$Users = Get-MgUser -All -Property $Properties - -$Export = foreach ($User in $Users) { - $AssignedLicenses = if ($User.AssignedLicenses.Count -gt 0) { - ($User.AssignedLicenses | ForEach-Object { - $SkuId = $_.SkuId.ToString() - if ($SkuLookup.ContainsKey($SkuId)) { - $SkuLookup[$SkuId] - } - else { - $SkuId - } - }) -join "; " - } - else { - "None" - } - - [PSCustomObject]@{ - Name = $User.DisplayName - UserPrincipalName = $User.UserPrincipalName - UserType = $User.UserType - AccountEnabled = $User.AccountEnabled - AssignedLicenses = $AssignedLicenses - GuestInvitationState = $User.ExternalUserState - LastInteractiveSignInDate = $User.SignInActivity.LastSignInDateTime - } -} - -$Export | - Sort-Object Name | - Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 - -Write-Host "Export complete: $OutputPath" \ No newline at end of file diff --git a/scripts/DNS Detection Script.ps1 b/scripts/DNS Detection Script.ps1 new file mode 100755 index 0000000..8290dfb --- /dev/null +++ b/scripts/DNS Detection Script.ps1 @@ -0,0 +1,90 @@ +<# +.SYNOPSIS + Audits active network adapters to verify malware-filtering DNS configuration. + +.DESCRIPTION + Retrieves all connected network adapters ('Up' status) and checks whether their configured + DNS server addresses match an approved list of security-filtering DNS providers + (e.g., Quad9 or Cloudflare for Families). + + Outputs a compressed JSON payload for RMM systems or Intune Custom Compliance discovery. + +.PARAMETER ApprovedDnsServers + Array of IPv4 DNS server addresses considered compliant. + Defaults to Quad9 (9.9.9.9, 149.112.112.11) and Cloudflare Security/Family (1.1.1.2, 1.0.0.2, 1.1.1.3, 1.0.0.3). + +.EXAMPLE + .\Test-DnsCompliance.ps1 + +.EXAMPLE + .\Test-DnsCompliance.ps1 -ApprovedDnsServers "9.9.9.9", "1.1.1.2" + +.NOTES + Created: 2025-03-05 + Modified: 2026-10-04 +#> + +[CmdletBinding()] +param ( + [string[]]$ApprovedDnsServers = @( + "9.9.9.9", # Quad9 Primary (Malware + DNSSEC) + "149.112.112.11", # Quad9 Secondary (Malware + DNSSEC) + "1.1.1.2", # Cloudflare Security Primary (Malware) + "1.0.0.2", # Cloudflare Security Secondary (Malware) + "1.1.1.3", # Cloudflare Family Primary (Malware + Adult) + "1.0.0.3" # Cloudflare Family Secondary (Malware + Adult) + ) +) + +# 1. Retrieve all active/connected network adapters +$ActiveAdapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } + +if (-not $ActiveAdapters) { + Write-Verbose "No active network adapters found on this endpoint." + $NoAdapterOutput = [ordered]@{ + Compliant = $true + Reason = "No active network adapters found." + Adapters = @() + } + return ($NoAdapterOutput | ConvertTo-Json -Compress) +} + +$IsOverallCompliant = $true +$AdapterAuditResults = [System.Collections.Generic.List[PSCustomObject]]::new() + +# 2. Audit DNS server assignments per active interface +foreach ($Adapter in $ActiveAdapters) { + + # Query assigned IPv4 DNS server addresses + $DnsConfig = Get-DnsClientServerAddress -InterfaceIndex $Adapter.InterfaceIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue + $ConfiguredServers = $DnsConfig.ServerAddresses + + # Verify if configured DNS contains at least one approved security DNS IP + $MatchingServers = $ConfiguredServers | Where-Object { $ApprovedDnsServers -contains $_ } + $IsAdapterCompliant = ($MatchingServers.Count -gt 0) + + # If any active adapter fails the check, mark the endpoint overall non-compliant + if (-not $IsAdapterCompliant) { + $IsOverallCompliant = $false + Write-Verbose "Adapter '$($Adapter.Name)' (Index: $($Adapter.InterfaceIndex)) is NON-COMPLIANT. Configured: $($ConfiguredServers -join ', ')" + } + else { + Write-Verbose "Adapter '$($Adapter.Name)' (Index: $($Adapter.InterfaceIndex)) is COMPLIANT. Configured: $($ConfiguredServers -join ', ')" + } + + $AdapterAuditResults.Add([PSCustomObject]@{ + AdapterName = $Adapter.Name + InterfaceIndex = $Adapter.InterfaceIndex + ConfiguredDns = $ConfiguredServers + IsCompliant = $IsAdapterCompliant + }) +} + +# 3. Construct summary object and output JSON +$ComplianceReport = [ordered]@{ + Compliant = $IsOverallCompliant + TestedAt = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + Adapters = $AdapterAuditResults +} + +return ($ComplianceReport | ConvertTo-Json -Compress) \ No newline at end of file diff --git a/scripts/DirectoryListing.ps1 b/scripts/DirectoryListing.ps1 new file mode 100755 index 0000000..e66d7ba --- /dev/null +++ b/scripts/DirectoryListing.ps1 @@ -0,0 +1,296 @@ +<# +.SYNOPSIS + Generates interactive HTML and plain-text directory tree reports. + +.DESCRIPTION + Recursively scans a target directory and produces two report files: + 1. A plain-text ASCII directory tree file (.txt). + 2. An interactive HTML tree file (.html) with search filtering and collapse/expand controls. + +.PARAMETER RootPath + The target root folder path to scan. + +.PARAMETER OutputDirectory + The destination directory where reports will be saved. Defaults to the current working directory. + +.PARAMETER ReportNamePrefix + Optional custom prefix for generated filenames. Defaults to the sanitized root folder name. + +.PARAMETER OpenInBrowser + Switch parameter to automatically open the generated HTML report upon completion. + +.EXAMPLE + .\Export-DirectoryTree.ps1 -RootPath "E:\Sync Method\Corporate\Documents" -OutputDirectory "C:\Reports" -OpenInBrowser + +.NOTES + Created: 2025-03-02 + Modified: 2026-10-04 +#> + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline =$true)] + [ValidateNotNullOrEmpty()] + [string]$RootPath, + + [Parameter(Mandatory = $false, Position = 1)] [string]$OutputDirectory = (Get-Location).Path, + + [Parameter(Mandatory = $false)] + [string]$ReportNamePrefix, + + [switch]$OpenInBrowser +) + +# 1. Validate & Resolve Paths +if (-not (Test-Path -LiteralPath $RootPath -PathType Container)) { + Write-Error "Target root directory not found or unreadable: '$RootPath'" + return +} + +$ResolvedRoot = (Get-Item -LiteralPath$RootPath).FullName +$RootLeafName = Split-Path -Path$ResolvedRoot -Leaf + +# Auto-generate report prefix if not explicitly provided +if ([string]::IsNullOrWhiteSpace($ReportNamePrefix)) { + $ReportNamePrefix =$RootLeafName -replace '[\/\\:\*\?"<>\|]', '-' +} + +# Ensure output directory exists +if (-not (Test-Path -LiteralPath $OutputDirectory)) { + New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null +} + +$TextFile = Join-Path -Path $OutputDirectory -ChildPath "$ReportNamePrefix-tree.txt" +$HtmlFile = Join-Path -Path $OutputDirectory -ChildPath "$ReportNamePrefix-tree.html" + +# 2. State Tracking Setup +$script:textLines = [System.Collections.Generic.List[string]]::new() +$script:html = [System.Text.StringBuilder]::new()$script:scanErrors = 0 +$script:totalFiles = 0$script:totalFolders = 0 + +function ConvertTo-HtmlEncoded { + param([string]$Text) + return [System.Net.WebUtility]::HtmlEncode($Text) +} + +function Add-TreeItems { + param( + [string]$Path, + [string]$Prefix + ) + + try { + $items = @( + Get-ChildItem -LiteralPath $Path -Force -ErrorAction Stop | + Sort-Object @{ Expression = { if ($_.PSIsContainer) { 0 } else { 1 } } }, Name + ) + } + catch { + $script:scanErrors++ + [void]$script:textLines.Add("$Prefix[Unable to read: $($_.Exception.Message)]") + [void]$script:html.AppendLine( + "
  • Unable to read: $(ConvertTo-HtmlEncoded$_.Exception.Message)
  • " + ) + return + } + + for ($index = 0; $index -lt $items.Count; $index++) { + $item =$items[$index]$isLast = $index -eq ($items.Count - 1) + $connector = if ($isLast) { '\-- ' } else { '|-- ' } + $nextPrefix =$Prefix + $(if ($isLast) { ' ' } else { '| ' }) + + $nameHtml = ConvertTo-HtmlEncoded$item.Name + $pathHtml = ConvertTo-HtmlEncoded$item.FullName + $isLink = -not [string]::IsNullOrWhiteSpace([string]$item.LinkType) + + if ($item.PSIsContainer -and -not $isLink) {$script:totalFolders++ + [void]$script:textLines.Add("$Prefix$connector$($item.Name)") + [void]$script:html.AppendLine( + "
  • 📁 $nameHtml
  • ') + } + elseif ($item.PSIsContainer -and $isLink) {$script:totalFolders++ + $target = @($item.Target) -join ', ' + [void]$script:textLines.Add("$Prefix$connector$($item.Name) [Link → $target]") + [void]$script:html.AppendLine( + "" + ) + } + else { + $script:totalFiles++ + [void]$script:textLines.Add("$Prefix$connector$($item.Name)") + [void]$script:html.AppendLine( + "
  • 📄 $nameHtml
  • " + ) + } + } +} + +# 3. Document Building +Write-Host "Scanning directory structure: $ResolvedRoot" -ForegroundColor Cyan + +$rootNameHtml = ConvertTo-HtmlEncoded $RootLeafName$rootPathHtml = ConvertTo-HtmlEncoded $ResolvedRoot$generated = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + +[void]$script:textLines.Add($ResolvedRoot) + +[void]$script:html.AppendLine(@" + + + + + +Directory Tree - $rootNameHtml + + + +

    Directory Tree Report

    +
    + Root: $rootPathHtml
    + Generated: $generated +
    +
    + + + +
    + + + + +"@) + +# 4. Save Output Files +$script:textLines \vert{} Set-Content -LiteralPath$TextFile -Encoding UTF8 +$script:html.ToString() \vert{} Set-Content -LiteralPath$HtmlFile -Encoding UTF8 + +# Output Summary +Write-Host "`nScan Completed Successfully:" -ForegroundColor Green +Write-Host " Folders Scanned : $script:totalFolders" +Write-Host " Files Cataloged : $script:totalFiles" +Write-Host " Unreadable Errors: $script:scanErrors" +Write-Host " Text Report : $TextFile" +Write-Host " HTML Report : $HtmlFile`n" + +if ($OpenInBrowser) { + Start-Process $HtmlFile +} \ No newline at end of file diff --git a/scripts/Download-All-SharePoint-Files.ps1 b/scripts/Download-All-SharePoint-Files.ps1 new file mode 100755 index 0000000..77d1591 --- /dev/null +++ b/scripts/Download-All-SharePoint-Files.ps1 @@ -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 + } +} \ No newline at end of file diff --git a/scripts/Export-DocuSignCompletedAgreements.ps1 b/scripts/Export-DocuSignCompletedAgreements.ps1 new file mode 100644 index 0000000..e76b2ec --- /dev/null +++ b/scripts/Export-DocuSignCompletedAgreements.ps1 @@ -0,0 +1,458 @@ +<# +.SYNOPSIS + Exports completed DocuSign envelopes and Certificates of Completion via the eSignature REST API. + +.DESCRIPTION + Uses the DocuSign eSignature REST API to: + 1. Authenticate with JWT Bearer assertion and handle automatic token renewal. + 2. Enumerate completed envelopes across a specified date range with API pagination. + 3. Download each envelope as an archive ZIP or combined PDF. + 4. Support incremental runs by skipping previously downloaded files. + 5. Output a detailed CSV export log and execution summary. + +.PARAMETER IntegrationKey + The DocuSign App / Integration Key (Client ID). + +.PARAMETER UserId + The API User ID (GUID) of the user being impersonated. + +.PARAMETER AccountId + The DocuSign Account ID (GUID). + +.PARAMETER PrivateKeyPath + Path to the RSA private key PEM file associated with the Integration Key. + +.PARAMETER FromDate + Start date for querying completed envelopes. + +.PARAMETER ToDate + End date for querying completed envelopes. Defaults to the current date/time. + +.PARAMETER OutputRoot + Root directory where downloads and logs will be saved. + +.PARAMETER Environment + DocuSign environment target: 'Production' or 'Demo'. Defaults to 'Production'. + +.PARAMETER OutputFormat + Format to download documents: 'ArchiveZip' (ZIP containing all docs + certificate) or 'CombinedPdf' (Single combined PDF). + +.PARAMETER SkipExisting + If specified, skips downloading envelopes whose target file already exists locally. Defaults to $true. + +.PARAMETER PageSize + Number of envelopes to request per API page call. Defaults to 1000 (DocuSign max limit). + +.PARAMETER RetryCount + Maximum retry attempts for transient API errors or rate-limit throttling. Defaults to 4. + +.EXAMPLE + .\Export-DocuSignCompletedAgreements.ps1 ` + -IntegrationKey "00000000-0000-0000-0000-000000000000" ` + -UserId "11111111-1111-1111-1111-111111111111" ` + -AccountId "22222222-2222-2222-2222-222222222222" ` + -PrivateKeyPath "C:\Keys\docusign_private.key" ` + -FromDate "2024-01-01" ` + -OutputRoot "D:\DocuSignExport" ` + -OutputFormat ArchiveZip + +.NOTES + Created: 2025-03-05 + Modified: 2026-10-04 +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$IntegrationKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$UserId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$AccountId, + + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] + [string]$PrivateKeyPath, + + [Parameter(Mandatory = $true)] + [datetime]$FromDate, + + [Parameter(Mandatory = $false)] + [datetime]$ToDate = (Get-Date), + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$OutputRoot, + + [Parameter(Mandatory = $false)] + [ValidateSet("Production", "Demo")] + [string]$Environment = "Production", + + [Parameter(Mandatory = $false)] + [ValidateSet("ArchiveZip", "CombinedPdf")] + [string]$OutputFormat = "ArchiveZip", + + [Parameter(Mandatory = $false)] + [bool]$SkipExisting = $true, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, 1000)] + [int]$PageSize = 1000, + + [Parameter(Mandatory = $false)] + [int]$RetryCount = 4 +) + +$ErrorActionPreference = "Stop" + +# Global state tracking for OAuth token management +$script:currentToken = $null +$script:tokenExpiration = [DateTimeOffset]::MinValue +$script:apiBaseUri = $null + +# Helper Functions + +function ConvertTo-Base64Url { + param([byte[]]$Bytes) + [Convert]::ToBase64String($Bytes).TrimEnd("=").Replace("+", "-").Replace("/", "_") +} + +function Import-RsaPrivateKey { + param([string]$Path) + + $pem = Get-Content -Path $Path -Raw + $rsa = [System.Security.Cryptography.RSA]::Create() + + if ($pem -match "-----BEGIN RSA PRIVATE KEY-----") { + $body = $pem -replace "-----BEGIN RSA PRIVATE KEY-----", "" -replace "-----END RSA PRIVATE KEY-----", "" -replace "\s", "" + $bytes = [Convert]::FromBase64String($body) + [void]$rsa.ImportRSAPrivateKey($bytes, [ref]0) + return $rsa + } + + if ($pem -match "-----BEGIN PRIVATE KEY-----") { + $body = $pem -replace "-----BEGIN PRIVATE KEY-----", "" -replace "-----END PRIVATE KEY-----", "" -replace "\s", "" + $bytes = [Convert]::FromBase64String($body) + [void]$rsa.ImportPkcs8PrivateKey($bytes, [ref]0) + return $rsa + } + + throw "Private key must be PEM formatted with 'BEGIN RSA PRIVATE KEY' or 'BEGIN PRIVATE KEY'." +} + +function New-DocuSignJwtAssertion { + param( + [string]$IntegrationKey, + [string]$UserId, + [string]$AuthServer, + [string]$PrivateKeyPath + ) + + $now = [DateTimeOffset]::UtcNow + $header = @{ + alg = "RS256" + typ = "JWT" + } | ConvertTo-Json -Compress + + $payload = @{ + iss = $IntegrationKey + sub = $UserId + aud = $AuthServer + iat = $now.ToUnixTimeSeconds() + exp = $now.AddMinutes(55).ToUnixTimeSeconds() + scope = "signature impersonation" + } | ConvertTo-Json -Compress + + $encodedHeader = ConvertTo-Base64Url ([Text.Encoding]::UTF8.GetBytes($header)) + $encodedPayload = ConvertTo-Base64Url ([Text.Encoding]::UTF8.GetBytes($payload)) + $unsignedToken = "$encodedHeader.$encodedPayload" + + $rsa = Import-RsaPrivateKey -Path $PrivateKeyPath + try { + $signature = $rsa.SignData( + [Text.Encoding]::UTF8.GetBytes($unsignedToken), + [Security.Cryptography.HashAlgorithmName]::SHA256, + [Security.Cryptography.RSASignaturePadding]::Pkcs1 + ) + } + finally { + $rsa.Dispose() + } + + return "$unsignedToken.$(ConvertTo-Base64Url $signature)" +} + +function Get-ValidAccessToken { + # Refresh token 5 minutes before actual expiry to avoid race conditions + if ($null -ne $script:currentToken -and [DateTimeOffset]::UtcNow -lt $script:tokenExpiration) { + return $script:currentToken + } + + Write-Verbose "Obtaining fresh DocuSign access token..." + $authServer = if ($Environment -eq "Demo") { "account-d.docusign.com" } else { "account.docusign.com" } + $tokenUri = "https://$authServer/oauth/token" + + $assertion = New-DocuSignJwtAssertion ` + -IntegrationKey $IntegrationKey ` + -UserId $UserId ` + -AuthServer $authServer ` + -PrivateKeyPath $PrivateKeyPath + + $tokenResponse = Invoke-WithRetry -RetryCount $RetryCount -ActionName "JWT Token Request" -ScriptBlock { + Invoke-RestMethod ` + -Method Post ` + -Uri $tokenUri ` + -ContentType "application/x-www-form-urlencoded" ` + -Body @{ + grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer" + assertion = $assertion + } + } + + $script:currentToken = $tokenResponse.access_token + # DocuSign tokens typically last 3600s (1 hr); refresh 5 mins early + $expiresInSeconds = [int]($tokenResponse.expires_in ?? 3600) - 300 + $script:tokenExpiration = [DateTimeOffset]::UtcNow.AddSeconds($expiresInSeconds) + + return $script:currentToken +} + +function Invoke-WithRetry { + param( + [scriptblock]$ScriptBlock, + [int]$RetryCount = 4, + [string]$ActionName = "DocuSign API request" + ) + + for ($attempt = 1; $attempt -le ($RetryCount + 1); $attempt++) { + try { + return & $ScriptBlock + } + catch { + $response = $_.Exception.Response + $statusCode = $null + + if ($response -and $response.StatusCode) { + $statusCode = [int]$response.StatusCode + } + + $retryable = $statusCode -in @(429, 500, 502, 503, 504) + if (-not $retryable -or $attempt -gt $RetryCount) { + throw + } + + # Honor DocuSign 'Retry-After' header if provided on HTTP 429 + $delaySeconds = 0 + if ($statusCode -eq 429 -and $response.Headers["Retry-After"]) { + [int]::TryParse($response.Headers["Retry-After"], [ref]$delaySeconds) | Out-Null + } + + if ($delaySeconds -le 0) { + $delaySeconds = [math]::Min(60, [math]::Pow(2, $attempt)) + } + + Write-Warning "$ActionName encountered HTTP $statusCode. Retrying in $delaySeconds seconds (Attempt $attempt of $RetryCount)..." + Start-Sleep -Seconds $delaySeconds + } + } +} + +function Get-SafeFileName { + param( + [string]$Text, + [int]$MaxLength = 100 + ) + + $invalid = [Regex]::Escape(([IO.Path]::GetInvalidFileNameChars() -join "")) + $safe = [Regex]::Replace($Text, "[$invalid]", "_") + $safe = [Regex]::Replace($safe, "\s+", " ").Trim() + + if ([string]::IsNullOrWhiteSpace($safe)) { + $safe = "Envelope" + } + if ($safe.Length -gt $MaxLength) { + $safe = $safe.Substring(0, $MaxLength).Trim() + } + return $safe +} + +function Save-EnvelopeDocument { + param( + [string]$Uri, + [string]$Path, + [string]$AcceptHeader, + [int]$RetryCount + ) + + Invoke-WithRetry -RetryCount $RetryCount -ActionName "Downloading document to '$Path'" -ScriptBlock { + $token = Get-ValidAccessToken + $headers = @{ + Authorization = "Bearer $token" + Accept = $AcceptHeader + } + Invoke-WebRequest -Uri $Uri -Headers $headers -OutFile $Path -ErrorAction Stop + } | Out-Null +} + +# Main Execution Flow + +# 1. Directory Initialization +$downloadFolder = Join-Path $OutputRoot "completed-agreements" +$logFolder = Join-Path $OutputRoot "logs" + +New-Item -ItemType Directory -Path $downloadFolder -Force | Out-Null +New-Item -ItemType Directory -Path $logFolder -Force | Out-Null + +$timestamp = Get-Date -Format "yyyyMMdd-HHmmss" +$logPath = Join-Path $logFolder "docusign-export-log-$timestamp.csv" + +# 2. Base URI Discovery +$authServer = if ($Environment -eq "Demo") { "account-d.docusign.com" } else { "account.docusign.com" } +$initialToken = Get-ValidAccessToken + +Write-Host "Discovering account base URI for Account ID: $AccountId..." -ForegroundColor Cyan +$userInfo = Invoke-WithRetry -RetryCount $RetryCount -ActionName "UserInfo Discovery" -ScriptBlock { + Invoke-RestMethod -Method Get -Uri "https://$authServer/oauth/userinfo" -Headers @{ Authorization = "Bearer $initialToken" } +} + +$account = $userInfo.accounts | Where-Object { $_.account_id -eq $AccountId } | Select-Object -First 1 +if (-not $account) { + throw "AccountId '$AccountId' was not found for the authenticated impersonated user." +} + +$baseUri = $account.base_uri.TrimEnd("/") +$script:apiBaseUri = "$baseUri/restapi/v2.1/accounts/$AccountId" + +# 3. Request Format Settings +$documentId = if ($OutputFormat -eq "ArchiveZip") { "archive" } else { "combined" } +$extension = if ($OutputFormat -eq "ArchiveZip") { ".zip" } else { ".pdf" } +$accept = if ($OutputFormat -eq "ArchiveZip") { "application/zip" } else { "application/pdf" } + +$fromIso = $FromDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") +$toIso = $ToDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") + +Write-Host "Querying completed envelopes from $fromIso to $toIso..." -ForegroundColor Cyan + +# 4. Enumerate Completed Envelopes (Paginated) +$allEnvelopes = [System.Collections.Generic.List[object]]::new() +$startPosition = 0 + +do { + $query = @{ + from_date = $fromIso + to_date = $toIso + status = "completed" + count = $PageSize + start_position = $startPosition + order_by = "completed" + order = "asc" + } + + $queryString = ($query.GetEnumerator() | ForEach-Object { + "{0}={1}" -f [Uri]::EscapeDataString($_.Key), [Uri]::EscapeDataString([string]$_.Value) + }) -join "&" + + $uri = "$($script:apiBaseUri)/envelopes?$queryString" + + $page = Invoke-WithRetry -RetryCount $RetryCount -ActionName "Fetching Envelope List Page (Offset: $startPosition)" -ScriptBlock { + $token = Get-ValidAccessToken + Invoke-RestMethod -Method Get -Uri $uri -Headers @{ Authorization = "Bearer $token" } + } + + if ($page.envelopes) { + foreach ($env in $page.envelopes) { + $allEnvelopes.Add($env) + } + } + + $returnedCount = if ($page.envelopes) { @($page.envelopes).Count } else { 0 } + Write-Host " Retrieved $returnedCount envelopes (Page Start: $startPosition)." -ForegroundColor Gray + + $startPosition += $PageSize + $totalSetSize = [int]($page.totalSetSize ?? 0) + +} while ($allEnvelopes.Count -lt $totalSetSize -and $returnedCount -gt 0) + +Write-Host "Total completed envelopes identified: $($allEnvelopes.Count)" -ForegroundColor Green + +# 5. Download Documents & Build Audit Log +$logRows = [System.Collections.Generic.List[object]]::new() +$counter = 0 + +foreach ($envelope in $allEnvelopes) { + $counter++ + $envelopeId = $envelope.envelopeId + $subject = Get-SafeFileName -Text $envelope.emailSubject -MaxLength 80 + + $completedRaw = if ($envelope.completedDateTime) { $envelope.completedDateTime } else { $envelope.statusChangedDateTime } + $completedDate = if ($completedRaw) { + ([datetime]$completedRaw).ToString("yyyy-MM-dd") + } else { + "unknown-date" + } + + $fileName = "$completedDate - $subject - $envelopeId$extension" + $filePath = Join-Path $downloadFolder $fileName + $downloadUri = "$($script:apiBaseUri)/envelopes/$envelopeId/documents/$documentId`?certificate=true" + + Write-Progress ` + -Activity "Downloading DocuSign Agreements" ` + -Status "Item $counter of$($allEnvelopes.Count):$subject" ` + -PercentComplete (($counter / [math]::Max(1, $allEnvelopes.Count)) * 100) + + $status = "Downloaded" + $errorMessage = "" + + # Check for incremental skip + if ($SkipExisting -and (Test-Path -LiteralPath $filePath)) { + $status = "Skipped (Exists)" + Write-Verbose "Skipping existing file: $filePath" + } + else { + try { + Save-EnvelopeDocument -Uri $downloadUri -Path $filePath -AcceptHeader $accept -RetryCount $RetryCount + } + catch { + $status = "Failed" + $errorMessage = $_.Exception.Message + Write-Warning "Failed to download envelope $envelopeId`: $errorMessage" + } + } + + $logRows.Add([PSCustomObject]@{ + EnvelopeId = $envelopeId + Status = $status + EnvelopeStatus = $envelope.status + CompletedDate = $completedRaw + Subject = $envelope.emailSubject + OutputFormat = $OutputFormat + OutputPath = $filePath + Error = $errorMessage + }) +} + +# 6. Save Export Summary Log +$logRows \vert{} Export-Csv -Path$logPath -NoTypeInformation -Encoding UTF8 + +$downloadedCount = @($logRows \vert{} Where-Object {$_.Status -eq "Downloaded" }).Count +$skippedCount = @($logRows \vert{} Where-Object {$_.Status -eq "Skipped (Exists)" }).Count +$failedCount = @($logRows \vert{} Where-Object {$_.Status -eq "Failed" }).Count + +Write-Host "`n========================================" -ForegroundColor Green +Write-Host "DocuSign Agreement Export Completed" -ForegroundColor Green +Write-Host "========================================" -ForegroundColor Green +Write-Host " Total Identified : $($allEnvelopes.Count)" +Write-Host " Downloaded : $downloadedCount" +Write-Host " Skipped Existing : $skippedCount" +Write-Host " Failed : $failedCount" +Write-Host " Files Saved To : $downloadFolder" +Write-Host " Audit Log Saved : $logPath`n" + +if ($failedCount -gt 0) { + Write-Warning "One or more envelope downloads failed. Review the CSV audit log for detailed error reasons." +} \ No newline at end of file diff --git a/scripts/MDM-Enrollment.ps1 b/scripts/MDM-Enrollment.ps1 new file mode 100755 index 0000000..d5cdf5a --- /dev/null +++ b/scripts/MDM-Enrollment.ps1 @@ -0,0 +1,40 @@ +#Requires -RunAsAdministrator + +<# +.SYNOPSIS + Enrolls the endpoint into Microsoft Intune / MDM. +.DESCRIPTION + Triggers the Windows auto-enrollment process via deviceenroller.exe. + Intended for company-owned endpoints. +#> + +# Prompt for confirmation (case-insensitive) +Write-Host "WARNING: This script will enroll the current device into MDM." -ForegroundColor Red +Write-Host "This is meant for company-owned devices only." -ForegroundColor Red +$answer = Read-Host "Do you want to continue? (Y/N)" + +if ($answer -notmatch '^[Yy](es)?$') { + Write-Host "Exiting script — Device will not be enrolled into MDM." -ForegroundColor Yellow + exit 0 +} + +Write-Host "Triggering Intune MDM enrollment..." -ForegroundColor Cyan + +try { + # Launch deviceenroller and wait for command trigger completion + $process = Start-Process -FilePath "$env:SystemRoot\System32\deviceenroller.exe" ` + -ArgumentList "/c /AutoEnrollMDM" ` + -Wait ` + -PassThru ` + -NoNewWindow + + if ($process.ExitCode -eq 0) { + Write-Host "MDM AutoEnrollment command initiated successfully." -ForegroundColor Green + Write-Host "Note: Enrollment processing occurs asynchronously in the background." -ForegroundColor Yellow + } else { + Write-Host "deviceenroller.exe exited with code: $($process.ExitCode)" -ForegroundColor Red + } +} catch { + Write-Host "Failed to launch deviceenroller.exe: $_" -ForegroundColor Red + exit 1 +} \ No newline at end of file diff --git a/scripts/Remove-ClassicTeams.ps1 b/scripts/Remove-ClassicTeams.ps1 new file mode 100644 index 0000000..d575054 --- /dev/null +++ b/scripts/Remove-ClassicTeams.ps1 @@ -0,0 +1,135 @@ +#Requires -RunAsAdministrator + +<# +.SYNOPSIS + Removes Classic Microsoft Teams and Teams Machine-Wide Installer. +.DESCRIPTION + Stops Classic Teams processes, uninstalls the Machine-Wide Installer via MSI, + removes per-user Classic Teams installations, cleans residual files/registry run keys, + and verifies that New Teams (MSTeams) remains intact. +#> + +$ErrorActionPreference = "Continue" + +Write-Host "Starting Classic Microsoft Teams cleanup..." -ForegroundColor Cyan + +# 1. Stop ONLY Classic Teams processes (preserve New Teams) +Get-Process -Name "Teams" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + +# Stop Update.exe processes originating specifically from Classic Teams directories +Get-Process -Name "Update" -ErrorAction SilentlyContinue | Where-Object { + $_.Path -like "*\Microsoft\Teams\Update.exe" +} | Stop-Process -Force -ErrorAction SilentlyContinue + +Start-Sleep -Seconds 3 + +# 2. Remove Teams Machine-Wide Installer from HKLM registry +$uninstallPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" +) + +$machineWideInstallers = foreach ($path in$uninstallPaths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue \vert{} Where-Object {$_.DisplayName -eq "Teams Machine-Wide Installer" + } +} + +foreach ($app in$machineWideInstallers) { + Write-Host "Found Teams Machine-Wide Installer: $($app.DisplayName) $($app.DisplayVersion)" -ForegroundColor Yellow + + if ($app.UninstallString -match "\{[A-Fa-f0-9\-]{36}\}") { + $productCode =$Matches[0] + Write-Host "Uninstalling Machine-Wide Installer via MSI product code: $productCode" -ForegroundColor Cyan + Start-Process -FilePath "msiexec.exe" -ArgumentList "/x $productCode /qn /norestart" -Wait -NoNewWindow + } + elseif ($app.UninstallString) { + Write-Host "Uninstalling via UninstallString: $($app.UninstallString)" -ForegroundColor Cyan + if ($app.UninstallString -match "msiexec") { + $args =$app.UninstallString -replace "msiexec.exe", "" -replace "/I", "/x" + Start-Process -FilePath "msiexec.exe" -ArgumentList "$args /qn /norestart" -Wait -NoNewWindow + } else { + Start-Process -FilePath "cmd.exe" -ArgumentList "/c `"$($app.UninstallString)`" /quiet /norestart" -Wait -NoNewWindow + } + } +} + +# 3. Clean per-user Classic Teams installations +$usersRoot = Join-Path$env:SystemDrive "Users" +$userProfiles = Get-ChildItem -Path $usersRoot -Directory -ErrorAction SilentlyContinue \vert{} Where-Object {$_.Name -notin @("Public", "Default", "Default User", "All Users", "defaultuser0") +} + +foreach ($profile in$userProfiles) { + $localTeamsFolder = Join-Path$profile.FullName "AppData\Local\Microsoft\Teams" + $roamingTeamsFolder = Join-Path$profile.FullName "AppData\Roaming\Microsoft\Teams" + $updateExe = Join-Path$localTeamsFolder "Update.exe" + + if (Test-Path $updateExe) { + Write-Host "Uninstalling Classic Teams for profile: $($profile.Name)" -ForegroundColor Cyan + Start-Process -FilePath $updateExe -ArgumentList "--uninstall -s" -Wait -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + + # Remove residual AppData folders (both Local and Roaming) + foreach ($folder in @($localTeamsFolder,$roamingTeamsFolder)) { + if (Test-Path $folder) { + Write-Host "Removing directory: $folder" -ForegroundColor Yellow + Remove-Item -Path $folder -Recurse -Force -ErrorAction SilentlyContinue + } + } +} + +# 4. Remove System-Wide ProgramData Leftovers +$leftoverPaths = @( + "$env:ProgramData\Microsoft\Teams", + "$env:ProgramData\Microsoft\TeamsMachineInstaller", + "$env:ProgramData\Microsoft\TeamsCustomData" +) + +foreach ($path in$leftoverPaths) { + if (Test-Path $path) { + Write-Host "Removing leftover ProgramData path: $path" -ForegroundColor Yellow + Remove-Item -Path $path -Recurse -Force -ErrorAction SilentlyContinue + } +} + +# 5. Clean Startup Registry Run Keys across loaded user hives +$hkuPath = "Registry::HKEY_USERS" +Get-ChildItem -Path $hkuPath -ErrorAction SilentlyContinue | ForEach-Object { + $runKey = Join-Path$_.PSPath "Software\Microsoft\Windows\CurrentVersion\Run" + if (Test-Path $runKey) { + $val = Get-ItemProperty -Path$runKey -Name "com.squirrel.teams.teams" -ErrorAction SilentlyContinue + if ($val) { + Write-Host "Removing Classic Teams startup reg key from hive: $($_.PSChildName)" -ForegroundColor Yellow + Remove-ItemProperty -Path $runKey -Name "com.squirrel.teams.teams" -ErrorAction SilentlyContinue + } + } +} + +# 6. Confirm New Teams (MSTeams) status +$newTeams = Get-AppxPackage -Name "MSTeams" -AllUsers -ErrorAction SilentlyContinue + +if ($newTeams) { + Write-Host "New Teams (MSTeams) is installed:" -ForegroundColor Green + $newTeams | Select-Object Name, Version, PackageFullName | Format-Table -AutoSize | Out-String | Write-Host +} else { + Write-Host "WARNING: New Teams (MSTeams) was not detected on this device." -ForegroundColor Red +} + +# 7. Final Verification +$remainingMachineWide = foreach ($path in$uninstallPaths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue \vert{} Where-Object {$_.DisplayName -eq "Teams Machine-Wide Installer" + } +} + +$remainingClassicFolders = foreach ($profile in$userProfiles) { + $localTeamsFolder = Join-Path$profile.FullName "AppData\Local\Microsoft\Teams" + if (Test-Path $localTeamsFolder) {$localTeamsFolder } +} + +if ($remainingMachineWide -or$remainingClassicFolders) { + Write-Host "Cleanup completed, but some Classic Teams remnants remain." -ForegroundColor Red + exit 1 +} else { + Write-Host "Classic Teams and Teams Machine-Wide Installer cleanup completed successfully." -ForegroundColor Green + exit 0 +} \ No newline at end of file diff --git a/scripts/Remove-DotNetCore31.ps1 b/scripts/Remove-DotNetCore31.ps1 new file mode 100644 index 0000000..c01d3a3 --- /dev/null +++ b/scripts/Remove-DotNetCore31.ps1 @@ -0,0 +1,120 @@ +#Requires -RunAsAdministrator + +<# +.SYNOPSIS + Removes unsupported Microsoft .NET Core / ASP.NET Core / Windows Desktop Runtime / SDK 3.1.x components. +.DESCRIPTION + Queries HKLM uninstall registry locations (64-bit and 32-bit) for .NET 3.1.x components, + executes quiet uninstallation for both MSI and Executable/Bundle bootstrappers, + cleans orphaned runtime directories, and verifies removal. +#> + +$ErrorActionPreference = "Continue" + +Write-Host "Starting .NET Core 3.1 remediation..." -ForegroundColor Cyan + +$registryPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" +) + +# Search pattern targeting 3.1.x runtimes, SDKs, targeting packs, and hosting bundles +$targetApps = foreach ($path in$registryPaths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { + ( + $_.DisplayName -match "Microsoft .*\b3\.1\.\d+\b" -or + $_.DisplayName -match "Microsoft \.NET (Core|SDK|Runtime|Windows Desktop|ASP\.NET Core).*3\.1" + ) -and ($_.UninstallString -or$_.QuietUninstallString) + } +} + +if (-not $targetApps) { + Write-Host "No .NET Core 3.1 components found. System is clean." -ForegroundColor Green + exit 0 +} + +Write-Host "Found $($targetApps.Count) .NET Core 3.1 component(s) to remove:" -ForegroundColor Yellow +foreach ($app in$targetApps) { + Write-Host " - $($app.DisplayName) ($($app.DisplayVersion))" -ForegroundColor Yellow +} + +foreach ($app in$targetApps) { + Write-Host "`nAttempting to remove: $($app.DisplayName)..." -ForegroundColor Cyan + + $uninstallCmd = if ($app.QuietUninstallString) { $app.QuietUninstallString } else { $app.UninstallString } + + if ($uninstallCmd -match "\{[A-Fa-f0-9\-]{36}\}") { + # Standard MSI Uninstall + $productCode = $Matches[0] + Write-Host "Executing MSI uninstall for GUID: $productCode" -ForegroundColor Gray + $proc = Start-Process -FilePath "msiexec.exe" -ArgumentList "/x $productCode /qn /norestart" -Wait -PassThru -NoNewWindow + } + else { + # Executable / WiX Burn Bootstrapper Uninstall + Write-Host "Executing Bootstrapper EXE uninstall..." -ForegroundColor Gray + + # Parse EXE path and arguments safely + if ($uninstallCmd -match '^"([^"]+)"(.*)$') { + $exePath = $Matches[1] + $exeArgs = "$($Matches[2]) /uninstall /quiet /norestart".Trim() + } + elseif ($uninstallCmd -match '^(\S+)(.*)$') { + $exePath = $Matches[1] + $exeArgs = "$($Matches[2]) /uninstall /quiet /norestart".Trim() + } + else { + $exePath = $uninstallCmd + $exeArgs = "/uninstall /quiet /norestart" + } + + $proc = Start-Process -FilePath $exePath -ArgumentList $exeArgs -Wait -PassThru -NoNewWindow -ErrorAction SilentlyContinue + } + + # Evaluate return codes (0 = Success, 3010 = Success, reboot required) + if ($proc.ExitCode -in @(0, 3010)) { + Write-Host "Successfully processed uninstall for $($app.DisplayName)." -ForegroundColor Green + } else { + Write-Host "Warning: Uninstall process returned exit code $($proc.ExitCode) for $($app.DisplayName)." -ForegroundColor Yellow + } +} + +# Clean residual 3.1.x folder remnants from dotnet installation directories +$dotnetPaths = @( + "$env:ProgramFiles\dotnet", + "${env:ProgramFiles(x86)}\dotnet" +) + +foreach ($basePath in $dotnetPaths) { + if (Test-Path $basePath) { + Get-ChildItem -Path $basePath -Recurse -Directory -ErrorAction SilentlyContinue | Where-Object { + $_.Name -like "3.1.*" + } | ForEach-Object { + Write-Host "Removing leftover directory: $($_.FullName)" -ForegroundColor Yellow + Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue + } + } +} + +# Final Verification +Write-Host "`nVerifying remaining .NET Core 3.1 components..." -ForegroundColor Cyan + +$remainingApps = foreach ($path in$registryPaths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { + ( + $_.DisplayName -match "Microsoft .*\b3\.1\.\d+\b" -or + $_.DisplayName -match "Microsoft \.NET (Core|SDK|Runtime|Windows Desktop|ASP\.NET Core).*3\.1" + ) -and ($_.UninstallString -or$_.QuietUninstallString) + } +} + +if ($remainingApps) { + Write-Host "ERROR: Some .NET Core 3.1 components remain installed:" -ForegroundColor Red + $remainingApps | ForEach-Object { + Write-Host " - $($_.DisplayName) ($($_.DisplayVersion))" -ForegroundColor Red + } + exit 1 +} +else { + Write-Host ".NET Core 3.1 remediation completed successfully." -ForegroundColor Green + exit 0 +} \ No newline at end of file diff --git a/scripts/Remove-SpecificTeamsVersions.ps1 b/scripts/Remove-SpecificTeamsVersions.ps1 new file mode 100644 index 0000000..778f3d0 --- /dev/null +++ b/scripts/Remove-SpecificTeamsVersions.ps1 @@ -0,0 +1,193 @@ +#Requires -RunAsAdministrator + +<# +.SYNOPSIS + Removes specific vulnerable/outdated versions of Classic Teams and New Teams (MSTeams). +.DESCRIPTION + Checks for targeted build numbers across Classic Teams (Machine-Wide Installer, user AppData) + and New Teams (MSIX AppX provisioned and per-user packages), cleanly uninstalling only + the matching versions while leaving compliant versions untouched. +#> + +$ErrorActionPreference = "Continue" + +Write-Host "Starting targeted Microsoft Teams cleanup..." -ForegroundColor Cyan + +# Targeted vulnerable versions (includes Classic Teams 1.x and New Teams 23x/24x/25x builds) +$TargetVersions = @( + "24334.1103.3302.5694", + "25045.406.3471.2143", + "24124.2402.2858.5617", + "24004.1403.2634.2418", + "23195.1506.2253.6778", + "24277.3102.3183.2670", + "24256.2502.3123.1", + "24165.1306.2986.9504", + "25031.702.3408.1909", + "24244.507.3118.4732", + "23119.303.2080.2726", + "1.7.0.7956", + "25044.2208.3471.2155", + "24348.802.3311.5092", + "23335.242.2641.4129" +) + +# Helper function for version string normalization & comparison +function Test-IsTargetVersion { + param ( + [string]$VersionString, + [string[]]$TargetList + ) + if ([string]::IsNullOrWhiteSpace($VersionString)) { return$false } + + $cleanVersion =$VersionString.Trim() + if ($TargetList -contains $cleanVersion) { return$true } + + try { + $parsedVersion = [version]$cleanVersion + foreach ($target in$TargetList) { + try { + if ([version]$target -eq $parsedVersion) { return$true } + } catch {} + } + } catch {} + + return $false +} + +# 1. Safely stop targeted processes +Write-Host "Checking for running processes..." -ForegroundColor Cyan +Get-Process -Name "Teams" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue +Get-Process -Name "Update" -ErrorAction SilentlyContinue | Where-Object { + $_.Path -like "*\Microsoft\Teams\Update.exe" +} | Stop-Process -Force -ErrorAction SilentlyContinue + +# Stop ms-teams ONLY if the running executable belongs to a target version +Get-Process -Name "ms-teams" -ErrorAction SilentlyContinue | ForEach-Object { + $procVersion =$_.MainModule.FileVersionInfo.FileVersion + if (Test-IsTargetVersion -VersionString $procVersion -TargetList$TargetVersions) { + Write-Host "Stopping New Teams process (running vulnerable version $procVersion)..." -ForegroundColor Yellow + Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue + } +} + +# 2. Remediate targeted Teams Machine-Wide Installer (HKLM) +Write-Host "`nScanning Teams Machine-Wide Installer..." -ForegroundColor Cyan +$UninstallRegistryPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" +) + +foreach ($path in $UninstallRegistryPaths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { + $_.DisplayName -eq "Teams Machine-Wide Installer" -and + (Test-IsTargetVersion -VersionString $_.DisplayVersion -TargetList $TargetVersions) + } | ForEach-Object { + Write-Host "Found targeted Machine-Wide Installer version: $($_.DisplayVersion)" -ForegroundColor Yellow + + if ($_.UninstallString -match "\{[A-Fa-f0-9\-]{36}\}") { + $productCode = $Matches[0] + Start-Process -FilePath "msiexec.exe" -ArgumentList "/x $productCode /qn /norestart" -Wait -NoNewWindow + } elseif ($_.UninstallString) { + Start-Process -FilePath "cmd.exe" -ArgumentList "/c `"$($_.UninstallString)`" /quiet /norestart" -Wait -NoNewWindow + } + } +} + +# 3. Remediate targeted Classic Teams (User Profiles) +Write-Host "`nScanning user profiles for Classic Teams..." -ForegroundColor Cyan +$usersRoot = Join-Path$env:SystemDrive "Users" +$userProfiles = Get-ChildItem -Path $usersRoot -Directory -ErrorAction SilentlyContinue \vert{} Where-Object {$_.Name -notin @("Public", "Default", "Default User", "All Users", "defaultuser0") +} + +foreach ($profile in$userProfiles) { + $localTeamsFolder = Join-Path$profile.FullName "AppData\Local\Microsoft\Teams" + $roamingTeamsFolder = Join-Path$profile.FullName "AppData\Roaming\Microsoft\Teams" + $currentExe = Join-Path$localTeamsFolder "current\Teams.exe" + $updateExe = Join-Path$localTeamsFolder "Update.exe" + + if (Test-Path $localTeamsFolder) { + $detectedVersion =$null + if (Test-Path $currentExe) { $detectedVersion = (Get-Item$currentExe).VersionInfo.FileVersion } + if (-not $detectedVersion -and (Test-Path$updateExe)) { $detectedVersion = (Get-Item$updateExe).VersionInfo.FileVersion } + + if (Test-IsTargetVersion -VersionString $detectedVersion -TargetList$TargetVersions) { + Write-Host "Removing Classic Teams ($detectedVersion) for profile: $($profile.Name)" -ForegroundColor Yellow + + if (Test-Path $updateExe) { + Start-Process -FilePath $updateExe -ArgumentList "--uninstall -s" -Wait -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + + Remove-Item -Path $localTeamsFolder -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path $roamingTeamsFolder -Recurse -Force -ErrorAction SilentlyContinue + } + } +} + +# 4. Remediate targeted New Teams (AppX / MSIX Packages) +Write-Host "`nScanning New Teams (MSTeams) MSIX packages..." -ForegroundColor Cyan + +# Remove provisioned package if version matches +Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | Where-Object { + $_.DisplayName -eq "MSTeams" -and (Test-IsTargetVersion -VersionString $_.Version -TargetList $TargetVersions) +} | ForEach-Object { + Write-Host "Removing targeted provisioned MSTeams package version: $($_.Version)" -ForegroundColor Yellow + Remove-AppxProvisionedPackage -Online -PackageName $_.PackageName -ErrorAction SilentlyContinue +} + +# Remove per-user AppX packages if version matches +Get-AppxPackage -Name "MSTeams" -AllUsers -ErrorAction SilentlyContinue | Where-Object { + Test-IsTargetVersion -VersionString $_.Version -TargetList $TargetVersions +} | ForEach-Object { + Write-Host "Removing targeted MSTeams AppX package version: $($_.Version) for all users" -ForegroundColor Yellow + Remove-AppxPackage -Package $_.PackageFullName -AllUsers -ErrorAction SilentlyContinue +} + +# 5. Clean Startup Run Keys across loaded user hives +$hkuPath = "Registry::HKEY_USERS" +Get-ChildItem -Path $hkuPath -ErrorAction SilentlyContinue | ForEach-Object { + $runKey = Join-Path $_.PSPath "Software\Microsoft\Windows\CurrentVersion\Run" + if (Test-Path $runKey) { + $val = Get-ItemProperty -Path $runKey -Name "com.squirrel.teams.teams" -ErrorAction SilentlyContinue + if ($val) { + Remove-ItemProperty -Path $runKey -Name "com.squirrel.teams.teams" -ErrorAction SilentlyContinue + } + } +} + +# 6. Final Verification +Write-Host "`nPerforming final verification..." -ForegroundColor Cyan + +$remainingMachineWide = foreach ($path in$UninstallRegistryPaths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue \vert{} Where-Object {$_.DisplayName -eq "Teams Machine-Wide Installer" -and + (Test-IsTargetVersion -VersionString $_.DisplayVersion -TargetList$TargetVersions) + } +} + +$remainingClassicFolders = foreach ($profile in$userProfiles) { + $exe = Join-Path$profile.FullName "AppData\Local\Microsoft\Teams\current\Teams.exe" + if (Test-Path $exe) { + $ver = (Get-Item$exe).VersionInfo.FileVersion + if (Test-IsTargetVersion -VersionString $ver -TargetList $TargetVersions) {$exe } + } +} + +$remainingNewTeams = Get-AppxPackage -Name "MSTeams" -AllUsers -ErrorAction SilentlyContinue | Where-Object { + Test-IsTargetVersion -VersionString $_.Version -TargetList$TargetVersions +} + +if ($remainingMachineWide -or $remainingClassicFolders -or$remainingNewTeams) { + Write-Host "ERROR: Some targeted Teams versions still remain on the endpoint." -ForegroundColor Red + exit 1 +} else { + Write-Host "Targeted Teams remediation completed successfully." -ForegroundColor Green + + # Display active/healthy New Teams version if installed + $healthyTeams = Get-AppxPackage -Name "MSTeams" -AllUsers -ErrorAction SilentlyContinue + if ($healthyTeams) { + Write-Host "Active, non-vulnerable New Teams installation detected:" -ForegroundColor Green + $healthyTeams | Select-Object Name, Version, PackageFullName | Format-Table -AutoSize | Out-String | Write-Host + } + exit 0 +} \ No newline at end of file diff --git a/scripts/User-Recertification.ps1 b/scripts/User-Recertification.ps1 new file mode 100644 index 0000000..cc0b1ee --- /dev/null +++ b/scripts/User-Recertification.ps1 @@ -0,0 +1,109 @@ +<# +.SYNOPSIS + Exports Microsoft 365 user recertification and licensing report using Microsoft Graph SDK. +.DESCRIPTION + Retrieves all M365 users, resolves license SKU IDs to readable part numbers, + extracts last interactive sign-in dates, and exports the dataset to CSV. +.EXAMPLE + .\Export-M365UserCertification.ps1 -OutputPath ".\M365-User-Certification-Export.csv" +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [string]$OutputPath = ".\M365-User-Certification-Export.csv" +) + +# Silence Graph cmdlet progress bars to drastically decrease execution time +$OriginalProgressPreference = $ProgressPreference +$ProgressPreference = 'SilentlyContinue' + +try { + # Check existing Microsoft Graph session + $CurrentContext = Get-MgContext + $RequiredScopes = @("User.Read.All", "Directory.Read.All", "AuditLog.Read.All") + + if (-not $CurrentContext) { + Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + Connect-MgGraph -Scopes $RequiredScopes + } + else { + Write-Host "Connected as $($CurrentContext.Account)" -ForegroundColor Green + } + + # Resolve relative path to absolute path + $ResolvedOutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) + + Write-Host "Building license SKU lookup table..." -ForegroundColor Cyan + $SkuLookup = @{} + Get-MgSubscribedSku -All | ForEach-Object { + $SkuLookup[$_.SkuId.ToString()] = $_.SkuPartNumber + } + + # Explicitly define target properties (signInActivity requires AuditLog.Read.All) + $Properties = @( + "id", + "displayName", + "userPrincipalName", + "userType", + "accountEnabled", + "assignedLicenses", + "externalUserState", + "signInActivity" + ) + + Write-Host "Fetching user data from Microsoft Graph..." -ForegroundColor Cyan + $Users = Get-MgUser -All -Property $Properties + + Write-Host "Processing $($Users.Count) users..." -ForegroundColor Cyan + + $Export = foreach ($User in $Users) { + # Resolve licenses + $AssignedLicenses = if ($User.AssignedLicenses -and $User.AssignedLicenses.Count -gt 0) { + ($User.AssignedLicenses | ForEach-Object { + $SkuId = $_.SkuId.ToString() + if ($SkuLookup.ContainsKey($SkuId)) { + $SkuLookup[$SkuId] + } + else { + $SkuId + } + }) -join "; " + } + else { + "None" + } + + # Safe null-check for SignInActivity + $LastSignIn = if ($User.SignInActivity -and $User.SignInActivity.LastSignInDateTime) { + $User.SignInActivity.LastSignInDateTime + } + else { + "Never / No Record" + } + + [PSCustomObject]@{ + Name = $User.DisplayName + UserPrincipalName = $User.UserPrincipalName + UserType = $User.UserType + AccountEnabled = $User.AccountEnabled + AssignedLicenses = $AssignedLicenses + GuestInvitationState = $User.ExternalUserState + LastInteractiveSignInDate = $LastSignIn + } + } + + # Export results to CSV + $Export | + Sort-Object Name | + Export-Csv -Path $ResolvedOutputPath -NoTypeInformation -Encoding UTF8 + + Write-Host "Export complete: $ResolvedOutputPath" -ForegroundColor Green + +} +catch { + Write-Error "An error occurred during execution: $_" +} +finally { + # Restore original progress preference + $ProgressPreference = $OriginalProgressPreference +} \ No newline at end of file