- Changed DashboardURL to production: https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp - Added SSL/TLS certificate bypass - Added TLS 1.2 protocol requirement 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
560 lines
19 KiB
PowerShell
560 lines
19 KiB
PowerShell
#Requires -RunAsAdministrator
|
|
<#
|
|
.SYNOPSIS
|
|
Remotely executes asset collection script on shopfloor PCs using WinRM over HTTPS.
|
|
|
|
.DESCRIPTION
|
|
This script uses WinRM HTTPS to securely execute the Update-PC-CompleteAsset.ps1 script
|
|
on multiple shopfloor PCs. It handles:
|
|
1. Secure HTTPS connections using wildcard certificates
|
|
2. Automatic FQDN resolution from hostnames
|
|
3. Credential management for remote connections
|
|
4. Parallel execution across multiple PCs
|
|
5. Error handling and logging for remote operations
|
|
6. Collection of results from each remote PC
|
|
|
|
.PARAMETER HostnameList
|
|
Array of computer hostnames (without domain suffix).
|
|
|
|
.PARAMETER HostnameListFile
|
|
Path to a text file containing hostnames (one per line, without domain suffix).
|
|
|
|
.PARAMETER Domain
|
|
Domain suffix for FQDNs (e.g., "logon.ds.ge.com").
|
|
Will construct FQDNs as: hostname.domain
|
|
|
|
.PARAMETER Credential
|
|
PSCredential object for authenticating to remote computers.
|
|
If not provided, will prompt for credentials.
|
|
|
|
.PARAMETER MaxConcurrent
|
|
Maximum number of concurrent remote sessions (default: 5).
|
|
|
|
.PARAMETER Port
|
|
HTTPS port for WinRM (default: 5986).
|
|
|
|
.PARAMETER ProxyURL
|
|
URL for the warranty proxy server (passed to remote script).
|
|
|
|
.PARAMETER DashboardURL
|
|
URL for the dashboard API (passed to remote script).
|
|
|
|
.PARAMETER SkipWarranty
|
|
Skip warranty lookups on remote PCs (passed to remote script).
|
|
|
|
.PARAMETER LogPath
|
|
Path for log files (default: .\logs\remote-collection-https.log).
|
|
|
|
.PARAMETER TestConnections
|
|
Test remote HTTPS connections without running the full collection.
|
|
|
|
.PARAMETER ScriptPath
|
|
Path to the Update-PC-CompleteAsset.ps1 script on remote computers.
|
|
Default: C:\Scripts\Update-PC-CompleteAsset.ps1
|
|
|
|
.PARAMETER SkipCertificateCheck
|
|
Skip SSL certificate validation (not recommended for production).
|
|
|
|
.EXAMPLE
|
|
# Collect from specific hostnames
|
|
.\Invoke-RemoteAssetCollection-HTTPS.ps1 -HostnameList @("PC001", "PC002") -Domain "logon.ds.ge.com"
|
|
|
|
.EXAMPLE
|
|
# Collect from hostnames in file
|
|
.\Invoke-RemoteAssetCollection-HTTPS.ps1 -HostnameListFile ".\shopfloor-hostnames.txt" -Domain "logon.ds.ge.com"
|
|
|
|
.EXAMPLE
|
|
# Test HTTPS connections only
|
|
.\Invoke-RemoteAssetCollection-HTTPS.ps1 -HostnameList @("PC001") -Domain "logon.ds.ge.com" -TestConnections
|
|
|
|
.EXAMPLE
|
|
# Use stored credentials
|
|
$cred = Get-Credential
|
|
.\Invoke-RemoteAssetCollection-HTTPS.ps1 -HostnameListFile ".\shopfloor-hostnames.txt" `
|
|
-Domain "logon.ds.ge.com" -Credential $cred
|
|
|
|
.NOTES
|
|
Author: System Administrator
|
|
Date: 2025-10-17
|
|
Version: 1.0
|
|
|
|
Prerequisites:
|
|
1. WinRM HTTPS must be configured on target computers (use Setup-WinRM-HTTPS.ps1)
|
|
2. Wildcard certificate installed on target computers
|
|
3. PowerShell 5.1 or later
|
|
4. Update-PC-CompleteAsset.ps1 must be present on target computers
|
|
5. Credentials with admin rights on target computers
|
|
6. Network connectivity to target computers on port 5986
|
|
|
|
Advantages over HTTP WinRM:
|
|
- Encrypted traffic (credentials and data)
|
|
- No TrustedHosts configuration required
|
|
- Better security posture for production environments
|
|
#>
|
|
|
|
param(
|
|
[Parameter(Mandatory=$false)]
|
|
[string[]]$HostnameList = @(),
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string]$HostnameListFile,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Domain,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[PSCredential]$Credential,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[int]$MaxConcurrent = 5,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[int]$Port = 5986,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string]$ProxyURL = "http://10.48.130.158/vendor-api-proxy.php",
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string]$DashboardURL = "https://tsgwp00525.rd.ds.ge.com/shopdb/api.asp",
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[switch]$SkipWarranty = $true,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string]$LogPath = ".\logs\remote-collection-https.log",
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[switch]$TestConnections = $false,
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[string]$ScriptPath = "C:\Scripts\Update-PC-CompleteAsset.ps1",
|
|
|
|
[Parameter(Mandatory=$false)]
|
|
[switch]$SkipCertificateCheck = $false
|
|
)
|
|
|
|
# =============================================================================
|
|
# SSL/TLS Certificate Bypass for HTTPS connections
|
|
# =============================================================================
|
|
try {
|
|
if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy').Type) {
|
|
Add-Type @"
|
|
using System.Net;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
public class TrustAllCertsPolicy : ICertificatePolicy {
|
|
public bool CheckValidationResult(
|
|
ServicePoint srvPoint, X509Certificate certificate,
|
|
WebRequest request, int certificateProblem) {
|
|
return true;
|
|
}
|
|
}
|
|
"@
|
|
}
|
|
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
|
|
} catch { }
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
|
|
# Initialize logging
|
|
function Initialize-Logging {
|
|
param([string]$LogPath)
|
|
|
|
$logDir = Split-Path $LogPath -Parent
|
|
if (-not (Test-Path $logDir)) {
|
|
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
|
}
|
|
|
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
Add-Content -Path $LogPath -Value "[$timestamp] Remote asset collection (HTTPS) started"
|
|
}
|
|
|
|
function Write-Log {
|
|
param([string]$Message, [string]$LogPath, [string]$Level = "INFO")
|
|
|
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
$logEntry = "[$timestamp] [$Level] $Message"
|
|
|
|
Add-Content -Path $LogPath -Value $logEntry
|
|
|
|
switch ($Level) {
|
|
"ERROR" { Write-Host $logEntry -ForegroundColor Red }
|
|
"WARN" { Write-Host $logEntry -ForegroundColor Yellow }
|
|
"SUCCESS" { Write-Host $logEntry -ForegroundColor Green }
|
|
default { Write-Host $logEntry -ForegroundColor White }
|
|
}
|
|
}
|
|
|
|
function Get-ComputerTargets {
|
|
param([string[]]$HostnameList, [string]$HostnameListFile, [string]$Domain)
|
|
|
|
$hostnames = @()
|
|
|
|
# Add hostnames from direct list
|
|
if ($HostnameList.Count -gt 0) {
|
|
$hostnames += $HostnameList
|
|
}
|
|
|
|
# Add hostnames from file
|
|
if (-not [string]::IsNullOrEmpty($HostnameListFile)) {
|
|
if (Test-Path $HostnameListFile) {
|
|
$fileHostnames = Get-Content $HostnameListFile |
|
|
Where-Object { $_.Trim() -ne "" -and -not $_.StartsWith("#") } |
|
|
ForEach-Object { $_.Trim() }
|
|
$hostnames += $fileHostnames
|
|
} else {
|
|
Write-Log "Hostname list file not found: $HostnameListFile" $LogPath "ERROR"
|
|
}
|
|
}
|
|
|
|
# Remove duplicates and construct FQDNs
|
|
$fqdns = $hostnames |
|
|
Sort-Object -Unique |
|
|
ForEach-Object {
|
|
$hostname = $_.Trim()
|
|
# Remove domain if already present
|
|
if ($hostname -like "*.$Domain") {
|
|
$hostname
|
|
} else {
|
|
"$hostname.$Domain"
|
|
}
|
|
}
|
|
|
|
return $fqdns
|
|
}
|
|
|
|
function Resolve-ComputerIP {
|
|
param([string]$FQDN)
|
|
|
|
try {
|
|
$result = Resolve-DnsName -Name $FQDN -Type A -ErrorAction Stop
|
|
if ($result -and $result[0].IPAddress) {
|
|
return $result[0].IPAddress
|
|
}
|
|
return $null
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Test-WinRMHTTPSConnection {
|
|
param([string]$ComputerName, [PSCredential]$Credential, [int]$Port, [bool]$SkipCertCheck)
|
|
|
|
try {
|
|
$sessionOptions = New-PSSessionOption -SkipCACheck:$SkipCertCheck -SkipCNCheck:$SkipCertCheck
|
|
|
|
$session = New-PSSession -ComputerName $ComputerName `
|
|
-Credential $Credential `
|
|
-UseSSL `
|
|
-Port $Port `
|
|
-SessionOption $sessionOptions `
|
|
-ErrorAction Stop
|
|
|
|
Remove-PSSession $session
|
|
return $true
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Test-RemoteScriptExists {
|
|
param([string]$ComputerName, [PSCredential]$Credential, [string]$ScriptPath, [int]$Port, [bool]$SkipCertCheck)
|
|
|
|
try {
|
|
$sessionOptions = New-PSSessionOption -SkipCACheck:$SkipCertCheck -SkipCNCheck:$SkipCertCheck
|
|
|
|
$result = Invoke-Command -ComputerName $ComputerName `
|
|
-Credential $Credential `
|
|
-UseSSL `
|
|
-Port $Port `
|
|
-SessionOption $sessionOptions `
|
|
-ScriptBlock {
|
|
param($Path)
|
|
Test-Path $Path
|
|
} -ArgumentList $ScriptPath
|
|
|
|
return $result
|
|
}
|
|
catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Invoke-RemoteAssetScript {
|
|
param(
|
|
[string]$ComputerName,
|
|
[PSCredential]$Credential,
|
|
[string]$ScriptPath,
|
|
[string]$ProxyURL,
|
|
[string]$DashboardURL,
|
|
[bool]$SkipWarranty,
|
|
[int]$Port,
|
|
[bool]$SkipCertCheck,
|
|
[string]$LogPath
|
|
)
|
|
|
|
try {
|
|
Write-Log "Starting asset collection on $ComputerName (HTTPS)" $LogPath "INFO"
|
|
|
|
$sessionOptions = New-PSSessionOption -SkipCACheck:$SkipCertCheck -SkipCNCheck:$SkipCertCheck
|
|
|
|
# Execute the script remotely
|
|
$result = Invoke-Command -ComputerName $ComputerName `
|
|
-Credential $Credential `
|
|
-UseSSL `
|
|
-Port $Port `
|
|
-SessionOption $sessionOptions `
|
|
-ScriptBlock {
|
|
param($ScriptPath, $ProxyURL, $DashboardURL, $SkipWarranty)
|
|
|
|
# Change to script directory
|
|
$scriptDir = Split-Path $ScriptPath -Parent
|
|
Set-Location $scriptDir
|
|
|
|
# Build parameters
|
|
$params = @{
|
|
ProxyURL = $ProxyURL
|
|
DashboardURL = $DashboardURL
|
|
}
|
|
|
|
if ($SkipWarranty) {
|
|
$params.SkipWarranty = $true
|
|
}
|
|
|
|
# Execute the script and capture output
|
|
try {
|
|
& $ScriptPath @params
|
|
return @{
|
|
Success = $true
|
|
Output = "Script completed successfully"
|
|
Error = $null
|
|
}
|
|
}
|
|
catch {
|
|
return @{
|
|
Success = $false
|
|
Output = $null
|
|
Error = $_.Exception.Message
|
|
}
|
|
}
|
|
} -ArgumentList $ScriptPath, $ProxyURL, $DashboardURL, $SkipWarranty
|
|
|
|
if ($result.Success) {
|
|
Write-Log "Asset collection completed successfully on $ComputerName" $LogPath "SUCCESS"
|
|
return @{ Success = $true; Computer = $ComputerName; Message = $result.Output }
|
|
} else {
|
|
Write-Log "Asset collection failed on $ComputerName: $($result.Error)" $LogPath "ERROR"
|
|
return @{ Success = $false; Computer = $ComputerName; Message = $result.Error }
|
|
}
|
|
}
|
|
catch {
|
|
$errorMsg = "Failed to execute on $ComputerName: $($_.Exception.Message)"
|
|
Write-Log $errorMsg $LogPath "ERROR"
|
|
return @{ Success = $false; Computer = $ComputerName; Message = $errorMsg }
|
|
}
|
|
}
|
|
|
|
function Show-SetupInstructions {
|
|
param([string]$Domain)
|
|
|
|
Write-Host "`n=== WinRM HTTPS Setup Instructions ===" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
Write-Host "On each target computer, run the Setup-WinRM-HTTPS.ps1 script:" -ForegroundColor Yellow
|
|
Write-Host ""
|
|
Write-Host " # With certificate PFX file:" -ForegroundColor Gray
|
|
Write-Host " `$certPass = ConvertTo-SecureString 'Password' -AsPlainText -Force" -ForegroundColor White
|
|
Write-Host " .\Setup-WinRM-HTTPS.ps1 -CertificatePath 'C:\Certs\wildcard.pfx' ``" -ForegroundColor White
|
|
Write-Host " -CertificatePassword `$certPass -Domain '$Domain'" -ForegroundColor White
|
|
Write-Host ""
|
|
Write-Host " # Or with existing certificate:" -ForegroundColor Gray
|
|
Write-Host " .\Setup-WinRM-HTTPS.ps1 -CertificateThumbprint 'THUMBPRINT' -Domain '$Domain'" -ForegroundColor White
|
|
Write-Host ""
|
|
Write-Host "This will:" -ForegroundColor Yellow
|
|
Write-Host " 1. Install/locate the wildcard certificate" -ForegroundColor White
|
|
Write-Host " 2. Create HTTPS listener on port 5986" -ForegroundColor White
|
|
Write-Host " 3. Configure Windows Firewall" -ForegroundColor White
|
|
Write-Host " 4. Enable WinRM service" -ForegroundColor White
|
|
Write-Host ""
|
|
}
|
|
|
|
# Main execution
|
|
try {
|
|
Write-Host "=== Remote Asset Collection Script (HTTPS) ===" -ForegroundColor Cyan
|
|
Write-Host "Starting at $(Get-Date)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
|
|
# Initialize logging
|
|
Initialize-Logging -LogPath $LogPath
|
|
|
|
# Get target computers
|
|
$fqdns = Get-ComputerTargets -HostnameList $HostnameList -HostnameListFile $HostnameListFile -Domain $Domain
|
|
|
|
if ($fqdns.Count -eq 0) {
|
|
Write-Log "No target computers specified. Use -HostnameList or -HostnameListFile parameter." $LogPath "ERROR"
|
|
Show-SetupInstructions -Domain $Domain
|
|
exit 1
|
|
}
|
|
|
|
Write-Log "Target computers (FQDNs): $($fqdns -join ', ')" $LogPath "INFO"
|
|
|
|
# Resolve IP addresses
|
|
Write-Host "`nResolving IP addresses..." -ForegroundColor Yellow
|
|
$resolvedComputers = @()
|
|
foreach ($fqdn in $fqdns) {
|
|
Write-Host "Resolving $fqdn..." -NoNewline
|
|
$ip = Resolve-ComputerIP -FQDN $fqdn
|
|
if ($ip) {
|
|
Write-Host " [$ip]" -ForegroundColor Green
|
|
$resolvedComputers += @{ FQDN = $fqdn; IP = $ip }
|
|
Write-Log "Resolved $fqdn to $ip" $LogPath "INFO"
|
|
} else {
|
|
Write-Host " [DNS FAILED]" -ForegroundColor Red
|
|
Write-Log "Failed to resolve $fqdn" $LogPath "WARN"
|
|
}
|
|
}
|
|
|
|
if ($resolvedComputers.Count -eq 0) {
|
|
Write-Log "No computers could be resolved via DNS" $LogPath "ERROR"
|
|
exit 1
|
|
}
|
|
|
|
# Get credentials if not provided
|
|
if (-not $Credential) {
|
|
Write-Host "`nEnter credentials for remote computer access:" -ForegroundColor Yellow
|
|
$Credential = Get-Credential
|
|
if (-not $Credential) {
|
|
Write-Log "No credentials provided" $LogPath "ERROR"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Test connections if requested
|
|
if ($TestConnections) {
|
|
Write-Host "`nTesting HTTPS connections only..." -ForegroundColor Yellow
|
|
foreach ($comp in $resolvedComputers) {
|
|
$fqdn = $comp.FQDN
|
|
Write-Host "Testing $fqdn..." -NoNewline
|
|
if (Test-WinRMHTTPSConnection -ComputerName $fqdn -Credential $Credential -Port $Port -SkipCertCheck $SkipCertificateCheck) {
|
|
Write-Host " [OK]" -ForegroundColor Green
|
|
Write-Log "HTTPS connection test successful for $fqdn" $LogPath "SUCCESS"
|
|
} else {
|
|
Write-Host " [FAIL]" -ForegroundColor Red
|
|
Write-Log "HTTPS connection test failed for $fqdn" $LogPath "ERROR"
|
|
}
|
|
}
|
|
exit 0
|
|
}
|
|
|
|
# Validate all connections and script existence before starting collection
|
|
Write-Host "`nValidating remote HTTPS connections and script availability..." -ForegroundColor Yellow
|
|
$validComputers = @()
|
|
|
|
foreach ($comp in $resolvedComputers) {
|
|
$fqdn = $comp.FQDN
|
|
Write-Host "Validating $fqdn..." -NoNewline
|
|
|
|
if (-not (Test-WinRMHTTPSConnection -ComputerName $fqdn -Credential $Credential -Port $Port -SkipCertCheck $SkipCertificateCheck)) {
|
|
Write-Host " [CONNECTION FAILED]" -ForegroundColor Red
|
|
Write-Log "Cannot connect to $fqdn via WinRM HTTPS" $LogPath "ERROR"
|
|
continue
|
|
}
|
|
|
|
if (-not (Test-RemoteScriptExists -ComputerName $fqdn -Credential $Credential -ScriptPath $ScriptPath -Port $Port -SkipCertCheck $SkipCertificateCheck)) {
|
|
Write-Host " [SCRIPT NOT FOUND]" -ForegroundColor Red
|
|
Write-Log "Script not found on $fqdn at $ScriptPath" $LogPath "ERROR"
|
|
continue
|
|
}
|
|
|
|
Write-Host " [OK]" -ForegroundColor Green
|
|
$validComputers += $comp
|
|
}
|
|
|
|
if ($validComputers.Count -eq 0) {
|
|
Write-Log "No valid computers found for data collection" $LogPath "ERROR"
|
|
Show-SetupInstructions -Domain $Domain
|
|
exit 1
|
|
}
|
|
|
|
Write-Log "Valid computers for collection: $($validComputers.FQDN -join ', ')" $LogPath "INFO"
|
|
|
|
# Execute asset collection
|
|
Write-Host "`nStarting asset collection on $($validComputers.Count) computers..." -ForegroundColor Cyan
|
|
Write-Host "Max concurrent sessions: $MaxConcurrent" -ForegroundColor Gray
|
|
Write-Host "Using HTTPS on port: $Port" -ForegroundColor Gray
|
|
Write-Host ""
|
|
|
|
$results = @()
|
|
$jobs = @()
|
|
$completed = 0
|
|
|
|
# Process computers in batches
|
|
for ($i = 0; $i -lt $validComputers.Count; $i += $MaxConcurrent) {
|
|
$batch = $validComputers[$i..($i + $MaxConcurrent - 1)]
|
|
|
|
Write-Host "Processing batch: $($batch.FQDN -join ', ')" -ForegroundColor Yellow
|
|
|
|
# Start jobs for current batch
|
|
foreach ($comp in $batch) {
|
|
$fqdn = $comp.FQDN
|
|
|
|
$job = Start-Job -ScriptBlock {
|
|
param($FQDN, $Credential, $ScriptPath, $ProxyURL, $DashboardURL, $SkipWarranty, $Port, $SkipCertCheck, $LogPath, $Functions)
|
|
|
|
# Import functions into job scope
|
|
Invoke-Expression $Functions
|
|
|
|
Invoke-RemoteAssetScript -ComputerName $FQDN -Credential $Credential `
|
|
-ScriptPath $ScriptPath -ProxyURL $ProxyURL -DashboardURL $DashboardURL `
|
|
-SkipWarranty $SkipWarranty -Port $Port -SkipCertCheck $SkipCertCheck -LogPath $LogPath
|
|
|
|
} -ArgumentList $fqdn, $Credential, $ScriptPath, $ProxyURL, $DashboardURL, $SkipWarranty, $Port, $SkipCertificateCheck, $LogPath, (Get-Content $PSCommandPath | Out-String)
|
|
|
|
$jobs += $job
|
|
}
|
|
|
|
# Wait for batch to complete
|
|
$jobs | Wait-Job | Out-Null
|
|
|
|
# Collect results
|
|
foreach ($job in $jobs) {
|
|
$result = Receive-Job $job
|
|
$results += $result
|
|
Remove-Job $job
|
|
$completed++
|
|
|
|
$computer = $result.Computer
|
|
if ($result.Success) {
|
|
Write-Host "[OK] $computer - Completed successfully" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "[FAIL] $computer - Failed: $($result.Message)" -ForegroundColor Red
|
|
}
|
|
}
|
|
|
|
$jobs = @()
|
|
Write-Host "Batch completed. Progress: $completed/$($validComputers.Count)" -ForegroundColor Gray
|
|
Write-Host ""
|
|
}
|
|
|
|
# Summary
|
|
$successful = ($results | Where-Object { $_.Success }).Count
|
|
$failed = ($results | Where-Object { -not $_.Success }).Count
|
|
|
|
Write-Host "=== Collection Summary ===" -ForegroundColor Cyan
|
|
Write-Host "Total computers: $($validComputers.Count)" -ForegroundColor White
|
|
Write-Host "Successful: $successful" -ForegroundColor Green
|
|
Write-Host "Failed: $failed" -ForegroundColor Red
|
|
|
|
if ($failed -gt 0) {
|
|
Write-Host "`nFailed computers:" -ForegroundColor Yellow
|
|
$results | Where-Object { -not $_.Success } | ForEach-Object {
|
|
Write-Host " $($_.Computer): $($_.Message)" -ForegroundColor Red
|
|
}
|
|
}
|
|
|
|
Write-Log "Collection completed. Success: $successful, Failed: $failed" $LogPath "INFO"
|
|
|
|
} catch {
|
|
Write-Log "Fatal error: $($_.Exception.Message)" $LogPath "ERROR"
|
|
exit 1
|
|
}
|