Загрузка данных
#requires -version 5.1
<#
MTS Link uninstall audit
------------------------
НИЧЕГО НЕ УДАЛЯЕТ.
Ищет MTS Link:
- HKLM, 64-bit registry view
- HKLM, 32-bit registry view
- HKU для загруженных пользователей
- NTUSER.DAT для незагруженных пользовательских профилей
Собирает:
- DisplayName
- DisplayVersion
- Publisher
- InstallLocation
- UninstallString
- QuietUninstallString
- MSI ProductCode
- Registry path
- User SID / profile
- Scope
- 32/64-bit registry view
MTS Link Meetings 1.6.0.0 -> KEEP
всё остальное MTS Link -> REMOVE_LATER
#>
[CmdletBinding()]
param(
[string]$OutputDirectory = "$env:ProgramData\MTSLink-Audit"
)
$ErrorActionPreference = 'Stop'
# ----------------------------------------------------------------------
# Настройки
# ----------------------------------------------------------------------
$KeepProductName = 'MTS Link Meetings'
$KeepProductVersion = '1.6.0.0'
# Ловим и английское "MTS Link", и русское "МТС Линк"
$MtsNameRegex = '^(?i:(MTS\s*Link|МТС\s*Линк))'
$Results = New-Object System.Collections.Generic.List[object]
$Warnings = New-Object System.Collections.Generic.List[string]
# ----------------------------------------------------------------------
# Вспомогательные функции
# ----------------------------------------------------------------------
function Get-RegValue {
param(
[Microsoft.Win32.RegistryKey]$Key,
[string]$Name
)
try {
return $Key.GetValue(
$Name,
$null,
[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames
)
}
catch {
return $null
}
}
function Get-ProductCodeFromString {
param(
[string]$Text,
[string]$KeyName
)
# Если имя uninstall-раздела уже GUID
if ($KeyName -match '^\{[0-9A-Fa-f-]{36}\}$') {
return $KeyName
}
# Или GUID содержится в uninstall-команде
if ($Text -match '\{[0-9A-Fa-f-]{36}\}') {
return $Matches[0]
}
return $null
}
function Read-UninstallKey {
param(
[Microsoft.Win32.RegistryHive]$Hive,
[Microsoft.Win32.RegistryView]$View,
[string]$UninstallPath,
[ValidateSet('Machine','User')]
[string]$Scope,
[string]$UserSID,
[string]$ProfilePath,
[string]$LogicalRegistryRoot
)
$baseKey = $null
$uninstallKey = $null
try {
$baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($Hive, $View)
$uninstallKey = $baseKey.OpenSubKey($UninstallPath)
if (-not $uninstallKey) {
return
}
foreach ($subKeyName in $uninstallKey.GetSubKeyNames()) {
$appKey = $null
try {
$appKey = $uninstallKey.OpenSubKey($subKeyName)
if (-not $appKey) {
continue
}
$displayName = [string](Get-RegValue -Key $appKey -Name 'DisplayName')
if ([string]::IsNullOrWhiteSpace($displayName)) {
continue
}
if ($displayName -notmatch $MtsNameRegex) {
continue
}
$displayVersion = [string](Get-RegValue -Key $appKey -Name 'DisplayVersion')
$publisher = [string](Get-RegValue -Key $appKey -Name 'Publisher')
$installLocation = [string](Get-RegValue -Key $appKey -Name 'InstallLocation')
$installSource = [string](Get-RegValue -Key $appKey -Name 'InstallSource')
$uninstallString = [string](Get-RegValue -Key $appKey -Name 'UninstallString')
$quietUninstallString = [string](Get-RegValue -Key $appKey -Name 'QuietUninstallString')
$modifyPath = [string](Get-RegValue -Key $appKey -Name 'ModifyPath')
$windowsInstaller = Get-RegValue -Key $appKey -Name 'WindowsInstaller'
$productCode = Get-ProductCodeFromString `
-Text "$uninstallString $quietUninstallString $modifyPath" `
-KeyName $subKeyName
# Какая команда сейчас является наиболее подходящей зарегистрированной
# командой удаления. Ничего не запускаем.
$effectiveCommand = $null
$commandSource = $null
if (-not [string]::IsNullOrWhiteSpace($quietUninstallString)) {
$effectiveCommand = $quietUninstallString
$commandSource = 'QuietUninstallString'
}
elseif (-not [string]::IsNullOrWhiteSpace($uninstallString)) {
$effectiveCommand = $uninstallString
$commandSource = 'UninstallString'
}
# Только информационное поле.
# Эта команда НЕ запускается скриптом.
$derivedMsiCommand = $null
if (
$productCode -and
(
$windowsInstaller -eq 1 -or
$uninstallString -match '(?i)msiexec'
)
) {
$derivedMsiCommand =
"msiexec.exe /x $productCode /qn /norestart"
}
if (
$displayName -eq $KeepProductName -and
$displayVersion -eq $KeepProductVersion
) {
$action = 'KEEP'
}
else {
$action = 'REMOVE_LATER'
}
$viewName = if (
$View -eq [Microsoft.Win32.RegistryView]::Registry64
) {
'64-bit'
}
else {
'32-bit'
}
$logicalRegistryPath =
"$LogicalRegistryRoot\$subKeyName"
$Results.Add(
[PSCustomObject]@{
Action = $action
Scope = $Scope
UserSID = $UserSID
ProfilePath = $ProfilePath
DisplayName = $displayName
DisplayVersion = $displayVersion
Publisher = $publisher
RegistryView = $viewName
RegistryPath = $logicalRegistryPath
RegistryKeyName = $subKeyName
InstallLocation = $installLocation
InstallSource = $installSource
UninstallString = $uninstallString
QuietUninstallString = $quietUninstallString
EffectiveUninstallCommand = $effectiveCommand
EffectiveCommandSource = $commandSource
WindowsInstaller = $windowsInstaller
MsiProductCode = $productCode
# Только подсказка для следующего этапа.
# Сейчас НЕ выполняется.
DerivedMsiSilentCommand = $derivedMsiCommand
}
)
}
catch {
$Warnings.Add(
"Ошибка чтения $UninstallPath\$subKeyName : $($_.Exception.Message)"
)
}
finally {
if ($appKey) {
$appKey.Close()
$appKey.Dispose()
}
}
}
}
catch {
$Warnings.Add(
"Ошибка чтения registry path '$UninstallPath': $($_.Exception.Message)"
)
}
finally {
if ($uninstallKey) {
$uninstallKey.Close()
$uninstallKey.Dispose()
}
if ($baseKey) {
$baseKey.Close()
$baseKey.Dispose()
}
}
}
function Test-UserHiveLoaded {
param(
[string]$SID
)
$users = $null
$sidKey = $null
try {
$users = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
[Microsoft.Win32.RegistryHive]::Users,
[Microsoft.Win32.RegistryView]::Registry64
)
$sidKey = $users.OpenSubKey($SID)
return ($null -ne $sidKey)
}
catch {
return $false
}
finally {
if ($sidKey) {
$sidKey.Close()
$sidKey.Dispose()
}
if ($users) {
$users.Close()
$users.Dispose()
}
}
}
function Scan-UserHive {
param(
[string]$MountedHiveName,
[string]$RealSID,
[string]$ProfilePath
)
$relativePath =
"$MountedHiveName\Software\Microsoft\Windows\CurrentVersion\Uninstall"
$logicalPath =
"HKEY_USERS\$RealSID\Software\Microsoft\Windows\CurrentVersion\Uninstall"
foreach ($view in @(
[Microsoft.Win32.RegistryView]::Registry64,
[Microsoft.Win32.RegistryView]::Registry32
)) {
Read-UninstallKey `
-Hive ([Microsoft.Win32.RegistryHive]::Users) `
-View $view `
-UninstallPath $relativePath `
-Scope 'User' `
-UserSID $RealSID `
-ProfilePath $ProfilePath `
-LogicalRegistryRoot $logicalPath
}
}
# ----------------------------------------------------------------------
# 1. Машинные установки
# ----------------------------------------------------------------------
Write-Host 'Scanning machine-wide uninstall registry...'
foreach ($view in @(
[Microsoft.Win32.RegistryView]::Registry64,
[Microsoft.Win32.RegistryView]::Registry32
)) {
Read-UninstallKey `
-Hive ([Microsoft.Win32.RegistryHive]::LocalMachine) `
-View $view `
-UninstallPath 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' `
-Scope 'Machine' `
-UserSID $null `
-ProfilePath $null `
-LogicalRegistryRoot 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
}
# ----------------------------------------------------------------------
# 2. Получаем профили пользователей
# ----------------------------------------------------------------------
Write-Host 'Enumerating user profiles...'
$profileBase = $null
$profileList = $null
$Profiles = @()
try {
$profileBase = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
[Microsoft.Win32.RegistryHive]::LocalMachine,
[Microsoft.Win32.RegistryView]::Registry64
)
$profileList = $profileBase.OpenSubKey(
'SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
)
foreach ($sid in $profileList.GetSubKeyNames()) {
# Системные SID нам здесь не нужны
if ($sid -in @(
'S-1-5-18',
'S-1-5-19',
'S-1-5-20'
)) {
continue
}
$profileKey = $null
try {
$profileKey = $profileList.OpenSubKey($sid)
$profilePath = [string]$profileKey.GetValue('ProfileImagePath')
if ([string]::IsNullOrWhiteSpace($profilePath)) {
continue
}
$profilePath =
[Environment]::ExpandEnvironmentVariables($profilePath)
$ntUserDat = Join-Path $profilePath 'NTUSER.DAT'
# Если профиля физически нет, пропускаем
if (-not (Test-Path -LiteralPath $ntUserDat)) {
continue
}
$Profiles += [PSCustomObject]@{
SID = $sid
ProfilePath = $profilePath
NtUserDat = $ntUserDat
}
}
finally {
if ($profileKey) {
$profileKey.Close()
$profileKey.Dispose()
}
}
}
}
finally {
if ($profileList) {
$profileList.Close()
$profileList.Dispose()
}
if ($profileBase) {
$profileBase.Close()
$profileBase.Dispose()
}
}
# ----------------------------------------------------------------------
# 3. Пользовательские установки
# ----------------------------------------------------------------------
$counter = 0
foreach ($profile in $Profiles) {
$counter++
$sid = $profile.SID
$profilePath = $profile.ProfilePath
$ntUserDat = $profile.NtUserDat
Write-Host "Scanning user: $sid [$profilePath]"
if (Test-UserHiveLoaded -SID $sid) {
# Hive уже загружен, например пользователь сейчас залогинен.
Scan-UserHive `
-MountedHiveName $sid `
-RealSID $sid `
-ProfilePath $profilePath
continue
}
# ------------------------------------------------------------------
# Hive не загружен.
# Временно подключаем NTUSER.DAT только для чтения.
# ------------------------------------------------------------------
$safePid = $PID
$mountName = "MTSLinkAudit_${safePid}_${counter}"
$loadedByUs = $false
try {
Write-Host " Loading offline hive: $ntUserDat"
& "$env:SystemRoot\System32\reg.exe" `
load "HKU\$mountName" "$ntUserDat" 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
$Warnings.Add(
"Не удалось загрузить NTUSER.DAT для $sid : $ntUserDat"
)
continue
}
$loadedByUs = $true
Scan-UserHive `
-MountedHiveName $mountName `
-RealSID $sid `
-ProfilePath $profilePath
}
catch {
$Warnings.Add(
"Ошибка сканирования профиля $sid : $($_.Exception.Message)"
)
}
finally {
if ($loadedByUs) {
# На всякий случай освобождаем RegistryKey handles перед reg unload
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
& "$env:SystemRoot\System32\reg.exe" `
unload "HKU\$mountName" 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
$Warnings.Add(
"Не удалось выгрузить временный hive HKU\$mountName"
)
}
}
}
}
# ----------------------------------------------------------------------
# 4. Удаляем только полностью идентичные дубли из результата
# ----------------------------------------------------------------------
$Results = @(
$Results |
Sort-Object `
Scope,
UserSID,
DisplayName,
DisplayVersion,
RegistryView,
RegistryPath `
-Unique
)
# ----------------------------------------------------------------------
# 5. Сохраняем отчёт
# ----------------------------------------------------------------------
if (-not (Test-Path -LiteralPath $OutputDirectory)) {
New-Item `
-Path $OutputDirectory `
-ItemType Directory `
-Force | Out-Null
}
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$csvPath =
Join-Path $OutputDirectory "MTSLink_Audit_$timestamp.csv"
$jsonPath =
Join-Path $OutputDirectory "MTSLink_Audit_$timestamp.json"
$txtPath =
Join-Path $OutputDirectory "MTSLink_Audit_$timestamp.txt"
$Results |
Export-Csv `
-LiteralPath $csvPath `
-NoTypeInformation `
-Encoding UTF8 `
-Delimiter ';'
$Results |
ConvertTo-Json -Depth 5 |
Set-Content `
-LiteralPath $jsonPath `
-Encoding UTF8
$txt = New-Object System.Collections.Generic.List[string]
$txt.Add("ComputerName: $env:COMPUTERNAME")
$txt.Add("Audit time : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
$txt.Add("Found : $($Results.Count)")
$txt.Add("")
foreach ($item in $Results) {
$txt.Add('============================================================')
$txt.Add("Action : $($item.Action)")
$txt.Add("Name : $($item.DisplayName)")
$txt.Add("Version : $($item.DisplayVersion)")
$txt.Add("Scope : $($item.Scope)")
$txt.Add("RegistryView : $($item.RegistryView)")
$txt.Add("UserSID : $($item.UserSID)")
$txt.Add("Profile : $($item.ProfilePath)")
$txt.Add("Registry : $($item.RegistryPath)")
$txt.Add("")
$txt.Add("UninstallString:")
$txt.Add(" $($item.UninstallString)")
$txt.Add("")
$txt.Add("QuietUninstallString:")
$txt.Add(" $($item.QuietUninstallString)")
$txt.Add("")
$txt.Add("Effective command [$($item.EffectiveCommandSource)]:")
$txt.Add(" $($item.EffectiveUninstallCommand)")
if ($item.DerivedMsiSilentCommand) {
$txt.Add("")
$txt.Add("Derived MSI command - NOT EXECUTED:")
$txt.Add(" $($item.DerivedMsiSilentCommand)")
}
$txt.Add("")
}
if ($Warnings.Count -gt 0) {
$txt.Add('================ WARNINGS =================')
foreach ($warning in $Warnings) {
$txt.Add($warning)
}
}
$txt |
Set-Content `
-LiteralPath $txtPath `
-Encoding UTF8
# ----------------------------------------------------------------------
# 6. Вывод для SCCM / консоли
# ----------------------------------------------------------------------
Write-Host ''
Write-Host '================ MTS LINK AUDIT ================'
Write-Host "Computer : $env:COMPUTERNAME"
Write-Host "Found : $($Results.Count)"
Write-Host ''
if ($Results.Count -eq 0) {
Write-Host 'MTS Link products not found.'
}
else {
$Results |
Select-Object `
Action,
Scope,
DisplayName,
DisplayVersion,
RegistryView,
UserSID,
EffectiveCommandSource,
EffectiveUninstallCommand |
Format-Table -AutoSize |
Out-String -Width 500 |
Write-Host
Write-Host ''
Write-Host '----------- REGISTERED UNINSTALL COMMANDS -----------'
foreach ($item in $Results) {
Write-Host ''
Write-Host "[$($item.Action)] $($item.DisplayName) $($item.DisplayVersion)"
Write-Host "Scope : $($item.Scope)"
Write-Host "UserSID : $($item.UserSID)"
Write-Host "Registry: $($item.RegistryPath)"
Write-Host "UninstallString : $($item.UninstallString)"
Write-Host "QuietUninstallString : $($item.QuietUninstallString)"
Write-Host "Effective : $($item.EffectiveUninstallCommand)"
if ($item.DerivedMsiSilentCommand) {
Write-Host "Derived MSI - NOT RUN : $($item.DerivedMsiSilentCommand)"
}
}
}
Write-Host ''
Write-Host "CSV : $csvPath"
Write-Host "JSON: $jsonPath"
Write-Host "TXT : $txtPath"
if ($Warnings.Count -gt 0) {
Write-Host ''
Write-Host "Warnings: $($Warnings.Count)"
foreach ($warning in $Warnings) {
Write-Warning $warning
}
}
# Всегда 0: это пока аудит, не remediation.
exit 0