Загрузка данных
#requires -version 5.1
<#
MTS Link Audit v2
НИЧЕГО НЕ УДАЛЯЕТ.
Ищет зарегистрированные uninstall-команды:
- HKLM 64-bit
- HKLM 32-bit
- пользовательские HKU
- NTUSER.DAT незалогиненных пользователей
MTS Link Meetings 1.6.0.0 = KEEP
остальные MTS Link = REMOVE_LATER
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Continue'
# ============================================================
# Настройки
# ============================================================
$OutputDirectory = 'C:\ProgramData\MTSLink-Audit'
$KeepProductName = 'MTS Link Meetings'
$KeepProductVersion = '1.6.0.0'
# MTS Link / MTSLink / МТС Линк
$MtsNameRegex = '(?i)^(MTS\s*Link|МТС\s*Линк)'
$Timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
# ============================================================
# Папку создаём СРАЗУ
# ============================================================
try {
[System.IO.Directory]::CreateDirectory($OutputDirectory) | Out-Null
}
catch {
Write-Output "FATAL: Cannot create $OutputDirectory"
Write-Output $_.Exception.Message
exit 1
}
$LogPath = Join-Path $OutputDirectory "MTSLink_Audit_$Timestamp.log"
$CsvPath = Join-Path $OutputDirectory "MTSLink_Audit_$Timestamp.csv"
$TxtPath = Join-Path $OutputDirectory "MTSLink_Audit_$Timestamp.txt"
$Results = New-Object System.Collections.ArrayList
$Warnings = New-Object System.Collections.ArrayList
function Write-Log {
param(
[string]$Message
)
$line = '{0} {1}' -f (
Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
), $Message
try {
Add-Content `
-LiteralPath $LogPath `
-Value $line `
-Encoding UTF8 `
-ErrorAction SilentlyContinue
}
catch {
}
Write-Output $Message
}
function Add-WarningLog {
param(
[string]$Message
)
[void]$Warnings.Add($Message)
Write-Log "WARNING: $Message"
}
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-MsiProductCode {
param(
[string]$KeyName,
[string]$Text
)
if ($KeyName -match '^\{[0-9A-Fa-f-]{36}\}$') {
return $KeyName
}
if ($Text -match '\{[0-9A-Fa-f-]{36}\}') {
return $Matches[0]
}
return $null
}
function Invoke-RegExe {
param(
[string]$Arguments
)
$result = [ordered]@{
ExitCode = -1
StdOut = ''
StdErr = ''
}
try {
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName =
"$env:SystemRoot\System32\reg.exe"
$psi.Arguments = $Arguments
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $psi
[void]$process.Start()
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$process.WaitForExit()
$result.ExitCode = $process.ExitCode
$result.StdOut = $stdout.Trim()
$result.StdErr = $stderr.Trim()
$process.Dispose()
}
catch {
$result.StdErr = $_.Exception.Message
}
return [PSCustomObject]$result
}
function Test-HiveLoaded {
param(
[string]$SID
)
$base = $null
$key = $null
try {
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
[Microsoft.Win32.RegistryHive]::Users,
[Microsoft.Win32.RegistryView]::Registry64
)
$key = $base.OpenSubKey($SID)
if ($null -ne $key) {
return $true
}
return $false
}
catch {
return $false
}
finally {
if ($key) {
$key.Dispose()
}
if ($base) {
$base.Dispose()
}
}
}
function Read-UninstallRoot {
param(
[Microsoft.Win32.RegistryHive]$Hive,
[Microsoft.Win32.RegistryView]$View,
[string]$RegistryPath,
[string]$DisplayRegistryPath,
[ValidateSet('Machine','User')]
[string]$Scope,
[string]$SID,
[string]$ProfilePath
)
$base = $null
$root = $null
try {
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
$Hive,
$View
)
$root = $base.OpenSubKey($RegistryPath)
if ($null -eq $root) {
return
}
foreach ($subKeyName in $root.GetSubKeyNames()) {
$app = $null
try {
$app = $root.OpenSubKey($subKeyName)
if ($null -eq $app) {
continue
}
$displayName =
[string](Get-RegValue $app 'DisplayName')
if ([string]::IsNullOrWhiteSpace($displayName)) {
continue
}
if ($displayName -notmatch $MtsNameRegex) {
continue
}
$displayVersion =
[string](Get-RegValue $app 'DisplayVersion')
$publisher =
[string](Get-RegValue $app 'Publisher')
$installLocation =
[string](Get-RegValue $app 'InstallLocation')
$uninstallString =
[string](Get-RegValue $app 'UninstallString')
$quietUninstallString =
[string](Get-RegValue $app 'QuietUninstallString')
$modifyPath =
[string](Get-RegValue $app 'ModifyPath')
$windowsInstaller =
Get-RegValue $app 'WindowsInstaller'
$allCommandText =
"$uninstallString $quietUninstallString $modifyPath"
$productCode =
Get-MsiProductCode `
-KeyName $subKeyName `
-Text $allCommandText
# --------------------------------------------
# Какую зарегистрированную команду считать
# основной для будущего удаления
# --------------------------------------------
$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'
}
if (
$View -eq
[Microsoft.Win32.RegistryView]::Registry64
) {
$viewText = '64-bit'
}
else {
$viewText = '32-bit'
}
$item = [PSCustomObject]@{
Action = $action
Scope = $Scope
UserSID = $SID
ProfilePath = $ProfilePath
DisplayName = $displayName
DisplayVersion = $displayVersion
Publisher = $publisher
RegistryView = $viewText
RegistryPath =
"$DisplayRegistryPath\$subKeyName"
RegistryKeyName = $subKeyName
InstallLocation = $installLocation
UninstallString = $uninstallString
QuietUninstallString = $quietUninstallString
EffectiveCommandSource = $commandSource
EffectiveUninstallCommand =
$effectiveCommand
WindowsInstaller = $windowsInstaller
MsiProductCode = $productCode
DerivedMsiSilentCommand =
$derivedMsiCommand
}
[void]$Results.Add($item)
Write-Log (
"FOUND: [{0}] [{1}] {2} {3}" -f
$action,
$Scope,
$displayName,
$displayVersion
)
}
catch {
Add-WarningLog (
"Cannot read entry {0}\{1}: {2}" -f
$DisplayRegistryPath,
$subKeyName,
$_.Exception.Message
)
}
finally {
if ($app) {
$app.Dispose()
}
}
}
}
catch {
Add-WarningLog (
"Cannot scan registry path {0}: {1}" -f
$DisplayRegistryPath,
$_.Exception.Message
)
}
finally {
if ($root) {
$root.Dispose()
}
if ($base) {
$base.Dispose()
}
}
}
function Scan-UserHive {
param(
[string]$MountedName,
[string]$RealSID,
[string]$ProfilePath
)
$relativePath =
"$MountedName\Software\Microsoft\Windows\CurrentVersion\Uninstall"
$displayPath =
"HKEY_USERS\$RealSID\Software\Microsoft\Windows\CurrentVersion\Uninstall"
foreach ($view in @(
[Microsoft.Win32.RegistryView]::Registry64,
[Microsoft.Win32.RegistryView]::Registry32
)) {
Read-UninstallRoot `
-Hive ([Microsoft.Win32.RegistryHive]::Users) `
-View $view `
-RegistryPath $relativePath `
-DisplayRegistryPath $displayPath `
-Scope 'User' `
-SID $RealSID `
-ProfilePath $ProfilePath
}
}
# ============================================================
# START
# ============================================================
Write-Log '============================================================'
Write-Log 'MTS Link audit started'
Write-Log "Computer: $env:COMPUTERNAME"
Write-Log "Running as: $([Security.Principal.WindowsIdentity]::GetCurrent().Name)"
Write-Log "Output: $OutputDirectory"
Write-Log '============================================================'
# ============================================================
# 1. HKLM
# ============================================================
Write-Log 'Scanning machine-wide uninstall registry...'
foreach ($view in @(
[Microsoft.Win32.RegistryView]::Registry64,
[Microsoft.Win32.RegistryView]::Registry32
)) {
Read-UninstallRoot `
-Hive ([Microsoft.Win32.RegistryHive]::LocalMachine) `
-View $view `
-RegistryPath 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' `
-DisplayRegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' `
-Scope 'Machine' `
-SID $null `
-ProfilePath $null
}
# ============================================================
# 2. Получаем реальные пользовательские профили
# ============================================================
Write-Log 'Enumerating user profiles...'
$Profiles = @()
$profileBase = $null
$profileList = $null
try {
$profileBase =
[Microsoft.Win32.RegistryKey]::OpenBaseKey(
[Microsoft.Win32.RegistryHive]::LocalMachine,
[Microsoft.Win32.RegistryView]::Registry64
)
$profileList =
$profileBase.OpenSubKey(
'SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
)
if ($null -eq $profileList) {
throw 'ProfileList registry key not found'
}
foreach ($sid in $profileList.GetSubKeyNames()) {
# Системные учётки
if ($sid -in @(
'S-1-5-18',
'S-1-5-19',
'S-1-5-20'
)) {
continue
}
$profileKey = $null
try {
$profileKey = $profileList.OpenSubKey($sid)
if ($null -eq $profileKey) {
continue
}
$profilePath =
[string]$profileKey.GetValue(
'ProfileImagePath'
)
if ([string]::IsNullOrWhiteSpace($profilePath)) {
continue
}
$profilePath =
[Environment]::ExpandEnvironmentVariables(
$profilePath
)
# --------------------------------------------
# Не сканируем системные ServiceProfiles
# --------------------------------------------
if (
$profilePath.StartsWith(
$env:SystemRoot,
[System.StringComparison]::OrdinalIgnoreCase
)
) {
Write-Log "Skipping system profile: $profilePath"
continue
}
$ntUserDat =
Join-Path $profilePath 'NTUSER.DAT'
# Если hive уже загружен, NTUSER.DAT
# может быть занят — это нормально.
if (
-not (Test-HiveLoaded $sid) -and
-not (Test-Path -LiteralPath $ntUserDat)
) {
Write-Log "Skipping profile without NTUSER.DAT: $profilePath"
continue
}
$Profiles += [PSCustomObject]@{
SID = $sid
ProfilePath = $profilePath
NTUserDat = $ntUserDat
}
}
catch {
Add-WarningLog (
"Profile $sid enumeration failed: " +
$_.Exception.Message
)
}
finally {
if ($profileKey) {
$profileKey.Dispose()
}
}
}
}
catch {
Add-WarningLog (
"Profile enumeration failed: " +
$_.Exception.Message
)
}
finally {
if ($profileList) {
$profileList.Dispose()
}
if ($profileBase) {
$profileBase.Dispose()
}
}
# ============================================================
# 3. Сканируем пользовательские hive
# ============================================================
$counter = 0
foreach ($profile in $Profiles) {
$counter++
$sid = $profile.SID
$profilePath = $profile.ProfilePath
$ntUserDat = $profile.NTUserDat
Write-Log "Scanning user: $sid [$profilePath]"
# --------------------------------------------------------
# Hive уже загружен
# --------------------------------------------------------
if (Test-HiveLoaded $sid) {
Write-Log "Hive already loaded: HKEY_USERS\$sid"
try {
Scan-UserHive `
-MountedName $sid `
-RealSID $sid `
-ProfilePath $profilePath
}
catch {
Add-WarningLog (
"Loaded hive scan failed for $sid : " +
$_.Exception.Message
)
}
continue
}
# --------------------------------------------------------
# Hive не загружен
# --------------------------------------------------------
$mountName =
"MTSLinkAudit_${PID}_${counter}"
$loadedByScript = $false
try {
Write-Log "Loading offline hive: $ntUserDat"
$loadResult =
Invoke-RegExe (
'load "HKU\{0}" "{1}"' -f
$mountName,
$ntUserDat
)
if ($loadResult.ExitCode -ne 0) {
Add-WarningLog (
"reg.exe LOAD failed for $sid. " +
"ExitCode=$($loadResult.ExitCode); " +
"Error=$($loadResult.StdErr)"
)
continue
}
$loadedByScript = $true
Write-Log "Offline hive loaded as HKU\$mountName"
Scan-UserHive `
-MountedName $mountName `
-RealSID $sid `
-ProfilePath $profilePath
}
catch {
Add-WarningLog (
"Offline hive scan failed for $sid : " +
$_.Exception.Message
)
}
finally {
if ($loadedByScript) {
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
Start-Sleep -Milliseconds 200
$unloadResult =
Invoke-RegExe (
'unload "HKU\{0}"' -f
$mountName
)
if ($unloadResult.ExitCode -eq 0) {
Write-Log "Hive unloaded: HKU\$mountName"
}
else {
Add-WarningLog (
"reg.exe UNLOAD failed for HKU\$mountName. " +
"ExitCode=$($unloadResult.ExitCode); " +
"Error=$($unloadResult.StdErr)"
)
}
}
}
}
# ============================================================
# 4. Убираем точные дубли
# ============================================================
$FinalResults = @(
$Results |
Sort-Object `
Scope,
UserSID,
DisplayName,
DisplayVersion,
RegistryView,
RegistryPath `
-Unique
)
# ============================================================
# 5. CSV
# ============================================================
try {
$FinalResults |
Export-Csv `
-LiteralPath $CsvPath `
-NoTypeInformation `
-Encoding UTF8 `
-Delimiter ';'
Write-Log "CSV saved: $CsvPath"
}
catch {
Add-WarningLog (
"CSV save failed: " +
$_.Exception.Message
)
}
# ============================================================
# 6. TXT
# ============================================================
try {
$text = New-Object System.Collections.Generic.List[string]
$text.Add("ComputerName : $env:COMPUTERNAME")
$text.Add(
"AuditTime : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
)
$text.Add("Found : $($FinalResults.Count)")
$text.Add("Warnings : $($Warnings.Count)")
$text.Add('')
foreach ($item in $FinalResults) {
$text.Add(
'============================================================'
)
$text.Add("Action : $($item.Action)")
$text.Add("Name : $($item.DisplayName)")
$text.Add("Version : $($item.DisplayVersion)")
$text.Add("Scope : $($item.Scope)")
$text.Add("UserSID : $($item.UserSID)")
$text.Add("Profile : $($item.ProfilePath)")
$text.Add("RegView : $($item.RegistryView)")
$text.Add("Registry : $($item.RegistryPath)")
$text.Add("")
$text.Add('UninstallString:')
$text.Add(" $($item.UninstallString)")
$text.Add("")
$text.Add('QuietUninstallString:')
$text.Add(" $($item.QuietUninstallString)")
$text.Add("")
$text.Add(
"Effective [$($item.EffectiveCommandSource)]:"
)
$text.Add(
" $($item.EffectiveUninstallCommand)"
)
if ($item.DerivedMsiSilentCommand) {
$text.Add('')
$text.Add(
'Derived MSI command - INFORMATION ONLY, NOT EXECUTED:'
)
$text.Add(
" $($item.DerivedMsiSilentCommand)"
)
}
$text.Add('')
}
if ($Warnings.Count -gt 0) {
$text.Add('')
$text.Add(
'==================== WARNINGS ===================='
)
foreach ($warning in $Warnings) {
$text.Add($warning)
}
}
$text |
Set-Content `
-LiteralPath $TxtPath `
-Encoding UTF8
Write-Log "TXT saved: $TxtPath"
}
catch {
Add-WarningLog (
"TXT save failed: " +
$_.Exception.Message
)
}
# ============================================================
# 7. SCCM output
# ============================================================
Write-Log '============================================================'
Write-Log "AUDIT COMPLETE"
Write-Log "Found: $($FinalResults.Count)"
Write-Log "Warnings: $($Warnings.Count)"
Write-Log "Folder: $OutputDirectory"
Write-Log '============================================================'
foreach ($item in $FinalResults) {
Write-Output (
"{0} | {1} | {2} | {3} | {4}" -f
$item.Action,
$item.Scope,
$item.DisplayName,
$item.DisplayVersion,
$item.EffectiveUninstallCommand
)
}
# Аудит не считаем Failed из-за одного проблемного профиля
exit 0