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
"
- )
-
- Add-TreeItems -Path $item.FullName -Prefix $nextPrefix
-
- [void]$script:html.AppendLine('
')
- }
- elseif ($item.PSIsContainer -and $isLink) {
- $target = @($item.Target) -join ', '
- [void]$script:textLines.Add(
- "$Prefix$connector$($item.Name) [Link → $target]"
- )
- [void]$script:html.AppendLine(
- "🔗 $nameHtml → $(ConvertTo-HtmlEncoded $target)"
- )
- }
- 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
-
-
-
-
-
-
-
--
-
-📁 $rootNameHtml
-
-"@)
-
-Add-TreeItems -Path $root -Prefix ''
-
-[void]$script:html.AppendLine(@"
-
-
-
-
-
-
-
-"@)
-
-$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
"
+ )
+
+ # Recurse subdirectories
+ Add-TreeItems -Path $item.FullName -Prefix$nextPrefix
+
+ [void]$script:html.AppendLine('
')
+ }
+ 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(
+ "🔗 $nameHtml → $(ConvertTo-HtmlEncoded$target)"
+ )
+ }
+ 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
+
+
+
+
+
+
+
+-
+
+📁 $rootNameHtml
+
+"@)
+
+# Run recursive scan
+Add-TreeItems -Path $ResolvedRoot -Prefix ''
+
+[void]$script:html.AppendLine(@"
+
+
+
+
+
+
+
+"@)
+
+# 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