Tuesday, March 5, 2013

PowerShell: After ADMT Password Migration Fix

If you used Microsoft's Active Directory Migration Tool (ADMT) for any major migrations, then you know that migrated AD accounts are always set for user must change password upon next login. Well in the target domain we didn't want this setting set due to were migrating the password from the old domain and had complexity filter set so ADMT logs would tell us which accounts didn't meet the password criteria.

Below is the PowerShell code that met our after ADMT user migration fix needs. I used a switch statement on UAC so that account enabled status remained the same. Be advised that this code will set the user account so that user cannot change their password.

<#
Script Name: AD_After_ADMT_User_Migration_Fix.ps1
Version: 1.0
Author: Dean Bunn
Last Edited: 03/03/2013
Description: Corrects ADMT Account Changes Regarding Password Settings
#>

#Create NTAccounts for SELF and Everyone
$ntaSelf = New-Object System.Security.Principal.NTAccount("NT AUTHORITY","SELF");
$ntaEveryone = New-Object System.Security.Principal.NTAccount("Everyone");

#AD Security Types
$actDeny = [System.Security.AccessControl.AccessControlType]::Deny;
$adrER = [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight;

#GUID for Change Password AD Property
$gapCP = [Guid]"ab721a53-1e2f-11d0-9819-00aa0040529b";

#Create AD Deny Rules
$adrlDSPC = New-Object System.DirectoryServices.ActiveDirectoryAccessRule ($ntaSelf,$adrER,$actDeny,$gapCP);
$adrlDEPC = New-Object System.DirectoryServices.ActiveDirectoryAccessRule ($ntaEveryone,$adrER,$actDeny,$gapCP);

#Array of OUs to Run Against
$adOUs = @("OU=People,DC=myCollege,DC=edu",
           "OU=External,DC=myCollege,DC=edu");

#Var for Counting Accounts Checked
$nActCkd = 0;

foreach($adOU in $adOUs)
{
    #Var for OU ADsPath
    [string]$ouADsPath = "LDAP://" + $adOU;
    $deADOU = [ADSI]$ouADsPath;
    $dsSearch = New-Object DirectoryServices.DirectorySearcher($deADOU);
    $dsSearch.filter = "(&(objectClass=user)(sAMAccountName=*)(!objectClass=computer)(!objectClass=contact))";
    $dsSearch.PageSize = 900;
    $dsSearch.SearchScope = "SubTree";
    $srResults = $dsSearch.Findall();
    
    #Loop Through Search Results
    foreach($srResult in $srResults)
    {
        #Null Check on Search Result
        if($srResult)
        {
            #Increment Counting and Display Current Number
            $nActCkd++
            Write-Output $nActCkd.ToString();
            
            #Pull Directory Entry
            $deADUser = $srResult.GetDirectoryEntry();
            
            #Null Check on Directory Entry for User
            if($deADUser)
            {
                $deADUser.psbase.ObjectSecurity.AddAccessRule($adrlDSPC);
                $deADUser.psbase.ObjectSecurity.AddAccessRule($adrlDEPC);
                $deADUser.psbase.commitchanges();
                
                #Set Account to Not Expire (If Necessary)
                #$deADUser.accountExpires = 0;
                #$deADUser.setInfo();
            
                #Var for Account Status 
                [int]$uUAC = [int]::Parse($deADUser.userAccountControl.ToString());
        
                #Check for UAC Setting.
                switch($uUAC)
                {
                
                    512
                    {
                        #Set Password Never Expires
                        $deADUser.userAccountControl = 66048;
                        $deADUser.setInfo();
                    }
                
                    514
                    {
                        #Set Password Never Expires
                        $deADUser.userAccountControl = 66050;
                        $deADUser.setInfo();
                    }
                    
                    544 
                    {
                        #Set Password Never Expires
                        $deADUser.userAccountControl = 66048;
                        $deADUser.setInfo();
                    }
                    
                    546
                    {
                        #Set Password Never Expires
                        $deADUser.userAccountControl = 66050;
                        $deADUser.setInfo();
                    }
                    
                    66080 
                    { 
                        #Set Password Never Expires
                        $deADUser.userAccountControl = 66048;
                        $deADUser.setInfo();
                    }
                    
                    66082 
                    { 
                        #Set Password Never Expires
                        $deADUser.userAccountControl = 66050;
                        $deADUser.setInfo();
                    }
                        
                }#End of userAccountControl Switch
                
            }#End of Null Check on $deADUser
            
        }#End of Null Check on $srResult
        
    }#End of $srResults Foreach
    
}#End of $adOUs Foreach



Thursday, January 10, 2013

PowerShell: AD Export OU Structure

Below is the PowerShell code I used to export the OU structure between two domains. We only needed a few of the source base OUs exported and not the whole domain. The source OUs had hundreds of various nested level OUs underneath them. The base target OU needs to be created first and DN values of the OUs are case sensitive. The script can be run numerous time and will only create OUs in the target OU for OUs that don't exist.


#Create a HashTable to Hold OUs
$htOU = @{};

#Add Source and Target OU Pairs
$htOU["OU=PEOPLE,DC=OldDept,DC=myCollege,DC=edu"] = "OU=PEOPLE,OU=NewDept,DC=childDomain,DC=myCollege,DC=edu";
$htOU["OU=RESEARCH,DC=OldDept,DC=myCollege,DC=edu"] = "OU=RESEARCH,OU=NewDept,DC=childDomain,DC=myCollege,DC=edu";
$htOU["OU=EQUIPMENT,DC=OldDept,DC=myCollege,DC=edu"] = "OU=EQUIPMENT,OU=NewDept,DC=childDomain,DC=myCollege,DC=edu";


#Loop Through the OU HashTable
foreach($key in $htOU.keys)
{
       
    #Vars for OU DNs
    [string]$srcOUDN = $key.ToString();
    [string]$tgtOUDN = $htOU[$key].ToString().Trim();
    #Var for DN Path to Remove When Creating New Target OUs
    [string]$rmvPath = "," + $tgtOUDN;
    
    #Array To Hold Source OU DNs
    $arrSrcOUs = @();
    
    #Vars for ADsPath of Source and Target OUs
    [string]$srcADsPath = "LDAP://" + $srcOUDN;
    [string]$tgtADsPath = "LDAP://" + $tgtOUDN;
    
    #Retrieve Directory Entries for Source and Target OU
    $deSourceOU = [ADSI]$srcADsPath;
    $deTargetOU = [ADSI]$tgtADsPath;
    
    #Search Source OU for All OUs (Excluding Source OU)
    $dsSearch = New-Object DirectoryServices.DirectorySearcher($deSourceOU);
    $dsSearch.filter = "(&(objectClass=organizationalUnit)(!(distinguishedName=$srcOUDN)))";
    $dsSearch.PageSize = 900;
    $dsSearch.SearchScope = "SubTree";
    $srResults = $dsSearch.Findall();

    #Loop Through All Source OU Search Results
    foreach($srResult in $srResults)
    {      
        #Pull Directory Entry for Search Result and Store DN in Source OU Array
        $deOU = $srResult.GetDirectoryEntry();
        $arrSrcOUs += $deOU.distinguishedName.ToString();
    }
    
    #Loop Through Source OUs DN Values
    foreach($srcOU in $arrSrcOUs)
    {
        #Var for Target OU DN Path (Replacing Source OU Path with Target OU Path)
        [string]$uTgtOUDN = $srcOU.ToString().Replace($srcOUDN,$tgtOUDN);
        #Var for Target OU ADsPath (Used for Checking Existance)
        [string]$uTgtOUADsPath = "LDAP://" + $uTgtOUDN;
        
        #Check to See If Target OU Exists. If Not Create It.
        if(![ADSI]::Exists($uTgtOUADsPath))
        {
            #Add New OU and Save
            $newOU = $deTargetOU.create("organizationalUnit",$uTgtOUDN.Replace($rmvPath,""));
            $newOU.setInfo();
        }
                
    }#End of $arrSrcOUs Foreach

}#End of $htOU.keys Foreach

######### End of Script #############





 

Friday, November 23, 2012

PowerShell: Exiting Admin Account(s) Check

To prevent issues with when an admin leaves an organization and we disable or delete his or her AD account(s) which would then causes a service, scheduled task, or process running under those credentials on one of servers to fail. I created a PowerShell script to remotely check a list of servers for any service, process, or scheduled task using those AD account(s). The script has to be ran from an elevated PowerShell session.

##############################################################################
# Script Name: Exiting_Admin_Check.ps1
# Version: 1.0
# Author: Dean Bunn
# Last Edited: 11/22/2012
# Description: Check Remote Systems for Any Processes, Scheduled Tasks,
#               Servics Running with Credentials of an Exiting Admin
##############################################################################

#Error Handling Setting 
$ErrorActionPreference = "SilentlyContinue";

#Array of Domain\UserIDs to Check Against
[array]$userIDs = @("ad\adminAccount1","ad\adminAccount2","ad\adminAccount3");

#Vars for Email Notice
[string]$smtpServer = "smtpServer.mycollege.edu";
[string]$fromAddress = "scriptAccount@mycollege.edu";
[string]$toAddress = "adAdmins@mycollege.edu";

#Array of Server Names
$servers = @(   
                "SERVER1.MYCOLLEGE.EDU",
                "SERVER2.MYCOLLEGE.EDU",
                "SERVER3.MYCOLLEGE.EDU",
                "SERVER4.MYCOLLEGE.EDU",
                "SERVER5.MYCOLLEGE.EDU",
                "SERVER6.MYCOLLEGE.EDU"
            );

#Function for Checking Domain\UserIDs
function Check_Domain_Account([string]$dmnID,[array]$userIDs)
{
    
    foreach($userID in $userIDs)
    {
        #Check to See If Domain User IDs Match
        if([string]::Equals($userID.ToString().ToLower(),$dmnID.ToString().ToLower()))
        {
            return $true;
        }
    }

    return $false;
    
}#End of Check_Domain_Account Function

#Array to Hold Custom Reporting Objects
$summary = @();

foreach($server in $servers)
{
    #Vars for Reporting
    [string]$pingStatus = "";
    [string]$serviceStatus = "";
    [string]$processStatus = "";
    [string]$taskStatus = "";
    
    #Display Current Server Working Against
    Write-Output ("Working on " + $server.ToString().ToLower());
    
    #Ping Computer Before Attempting Remote WMI 
    if(test-connection -computername $server -quiet) 
    {
        $pingStatus = "passed";
        
        #Pull Services on Remote System
        $uServices = Get-WMIObject win32_service -Computer $server;
        
        #Null Check on Returned Services Collection
        if($uServices)
        {
            foreach($service in $uServices)
            {
                #Check the Account the Service is Running Under
                if(Check_Domain_Account $service.StartName.ToString() $userIDs)
                {
                    $serviceStatus = "found";
                    break;
                }
            }
        }
        else
        {
            $serviceStatus = "RPC failed";
        }#End of $uServices Null Check
        
        
        #Pull Processes from Remote System
        $uProcesses = Get-WmiObject win32_Process -ComputerName $server
        
        #Null Check on Return Processes Collection
        if($uProcesses)
        {
            foreach($process in $uProcesses)
            {
                #Pull Domain and User Info from Process
                $prcOwnerDomain = $process.GetOwner().Domain;
                $prcOwnerUserID = $process.GetOwner().User;
                #Verify that Both Domain and User ID Information Exists
                if($prcOwnerDomain -ne $null -and $prcOwnerUserID -ne $null)
                {
                    #Var for Domain\UserID Format
                    [string]$tmpDmnUserID = $prcOwnerDomain.ToString() + "\" + $prcOwnerUserID.ToString();
                    
                    #Check the Account the Process is Running As
                    if(Check_Domain_Account $tmpDmnUserID $userIDs)
                    {
                        $processStatus = "found";
                        break;
                    }
                }#End of Null Check on Domain and UserID
                
            }#End of Foreach Process
        }
        else
        {
            $processStatus = "RPC failed";
        }#End of $uProcesses Null Check
        
        #Scheduled Tasks
        try
        {
            #Connect to Schedule Service on Remote System and Pull Tasks
            $schedService = New-Object -ComObject Schedule.Service;
            $schedService.Connect($server);
            $rootTasks = $schedService.GetFolder("").GetTasks("");
            
            foreach ($task in $rootTasks) 
            { 
                #Create XML Object From Task XML Settings
                [XML]$taskXML = $task.Xml;
                    
                #Check to Account the Task will Run As
                if(Check_Domain_Account $taskXML.Task.Principals.Principal.UserId.ToString() $userIDs)
                {
                    $taskStatus = "found";
                    break;
                }
                    
            }#End of Foreach Task
            
        }
        catch
        {
            $taskStatus = "COM failed";
        }#End of Tasks Try\Catch

    }
    else
    {
        $pingStatus = "failed";
    }#End of Test Connection
    
    #Create Custom Reporting Object and Assign Values
    $uEntry = New-Object PSObject
    $uEntry | Add-Member -MemberType NoteProperty -Name "Server" -Value $server.ToString().ToLower();
    $uEntry | Add-Member -MemberType NoteProperty -Name "Ping_Status" -Value $pingStatus;
    $uEntry | Add-Member -MemberType NoteProperty -Name "Service_Status" -Value $serviceStatus;
    $uEntry | Add-Member -MemberType NoteProperty -Name "Process_Status" -Value $processStatus;
    $uEntry | Add-Member -MemberType NoteProperty -Name "Task_Status" -Value $taskStatus;
    #Add Reporting Object to Reporting Array
    $summary += $uEntry;
    
}#End of Foreach Computer

#Sort Report by Server Name
$summary = $summary | Sort-Object Server;

######## Configure HTML Report ########

#Var for HTML Message Body
$msgBody = "<html>
            <body>
            <h4>Exiting Admin Account(s) Report</h4>
            <span style=""font-size:8pt;font-family:Arial,sans-serif"">
            <strong>Accounts Checked:</strong>
            <br />";
            
#Add Each Account Checked to Report
foreach($usrID in $userIDs)
{
    $msgBody += $usrID.ToString().ToLower() + "<br />";
}

#Format the Report HTML Table
$msgBody += "</span><br />
             <table border=""1"" cellpadding=""4"" cellspacing=""0""
             style=""font-size:8pt;font-family:Arial,sans-serif"">
             <tr bgcolor=""#000099"">
             <td><strong><font color=""#FFFFFF"">Server</font></strong></td>
             <td align=""center""><strong><font color=""#FFFFFF"">Ping</font></strong></td>
             <td align=""center""><strong><font color=""#FFFFFF"">Services</font></strong></td>
             <td align=""center""><strong><font color=""#FFFFFF"">Processes</font></strong></td>
             <td align=""center""><strong><font color=""#FFFFFF"">Scheduled Tasks</font></strong></td>
             </tr>
             ";

#Var for Table Row Count
[int]$x = 1;

#Loop Through Custom Object Collection
foreach($srv in $summary)
{
    #Determine Even\Odd Row 
    if($x%2)
    {
        $msgBody += "<tr bgcolor=""#FFFFFF"">";
    }
    else
    {
        $msgBody += "<tr bgcolor=""#E8E8E8"">";
    }
    
    $x++;
    
    $msgBody += "<td>" `
                + $srv.Server `
                + "</td><td align=""center"">" `
                + $srv.Ping_Status `
                + "</td><td align=""center"">" `
                + $srv.service_Status `
                + "</td><td align=""center"">" `
                + $srv.process_Status `
                + "</td><td align=""center"">" `
                + $srv.task_Status `
                + "</td></tr>
                
                ";
}

#Close HTML Table and Message
$msgBody += "</table>
            </body>
            </html>";

#Settings for Email Message
$messageParameters = @{                        
                          Subject = "Exiting Admin Account(s) Report"
                           Body = $msgBody                       
                           From = $fromAddress                        
                           To = $toAddress                        
                           SmtpServer = $smtpServer                       
                       };                      
#Send Report Email Message 
Send-MailMessage @messageParameters –BodyAsHtml;

################################################################
#Export to CSV Section (Leaving In Case Needed Later)
#Get Current Short Date
#$rptDate = Get-Date -Format d;
#Configure Report Name
#$reportName = "Exiting_Admin_Check_" `
#              + $rptDate.ToString().Replace("/","-") + ".csv";
#Export Report to CSV
#$summary | Export-CSV $reportName -NoTypeInformation;
################################################################


Reference:
http://myitforum.com/cs2/blogs/yli628/archive/2008/07/28/powershell-script-to-retrieve-scheduled-tasks-on-a-remote-machine-task-scheduler-api.aspx

Thursday, November 15, 2012

PowerShell: Create Server Side Spam Rule

A few days ago I was tasked with creating a PowerShell script that would create a server side spam rule for all mailboxes in an 2010 environment. Using the Get-InboxRule and New-InboxRule cmdlets I was easily able to create a script that looked to see if the rule didn't already exist on the mailbox before it created it. The spam rule in this environment looks for a specific text in the header of the message. If it finds the text it will move the item to the Junk E-mail Folder.

Please be warned that if you use the New-InboxRule cmdlet on a mailbox it will remove any client side rules

##########################################################
# Script Name: Ex_Set_Inbox_Rule.ps1
# Version: 1.0
# Author: Dean Bunn
# Last Edited: 11/09/2012
# Description: Adds Spam Score Rule to Mailboxes
##########################################################

#Pull Collection of All Mailboxes 
$mbxs = Get-Mailbox -resultsize unlimited;

foreach($mbx in $mbxs)
{

    #Null Check on Primary SMTP Address
    if($mbx.PrimarySmtpAddress)
    {
        #Vars for New Inbox Rule
        [string]$primSMTP = $mbx.PrimarySmtpAddress.ToString();
        [string]$junkFolder = $mbx.PrimarySmtpAddress.ToString() + ":\Junk E-Mail";
        [string]$xscore = "X-Spam-Score: ****";
        [boolean]$existingRule = $false;
        
        #Retrieve Mailbox Rules
        $inboxRules = Get-InboxRule -mailbox $mbx.PrimarySmtpAddress.ToString();
        
        #Check to See If Any Rules Exist on Mailbox
        #If So Check for the SpamRule -ne $null -and $inboxRules.Count -gt 0
        if($inboxRules)
        {
             foreach($ibxr in $inboxRules)
            {
                if($ibxr.Name.ToString().Trim() -eq "SpamRule")
                {
                    $existingRule = $true;
                }
            }
            
        }
        
        #If Rule Doesn't Exist Create It
        if($existingRule -eq $false)
        {
            New-InboxRule -mailbox $primSMTP -Name "SpamRule" -Confirm:$False -MoveToFolder 
           $junkFolder -HeaderContainsWords @{add=$xscore} -StopProcessingRules $true;
        }
        
    }#End of Primary Address Check
    
}#End of Foreach Mailbox


References:
http://technet.microsoft.com/en-us/library/dd335170(v=exchg.141).aspx
http://technet.microsoft.com/en-us/library/bb684908.aspx

Wednesday, September 5, 2012

PowerShell: Get Report of Installed Applications Using Remoting

Below is a PowerShell script that uses PowerShell Remoting to pull a list of installed applications on a Windows system. Since the list is kept in the registry, you can easily use the Get-ChildItem cmdlet to take care of business. Found some resource links that helped me remove the Windows updates and hotfixes from the report.

##############################################################
# Script Name: PS_Remoting_Installed_Applications.ps1
# Version: 1.0
# Description: Using PowerShell Remoting Queries Remote
#               Systems for Installed Software
##############################################################

$allComputers = Invoke-Command -computername SERVER01,SERVER02,SERVER03 `
-scriptblock {  
                #Arrays for Holding Installed App Data 
                $installedApps = @();
                $installedAppsKeys = @();
                
                #Pull 32bit Installed Apps
                Get-ChildItem hklm:\Software\Microsoft\Windows\CurrentVersion\Uninstall `
                    | ForEach-Object { $installedAppsKeys += Get-ItemProperty $_.pspath `
                    | Where-Object {$_.DisplayName -and !$_.ReleaseType -and `
                    !$_.ParentKeyName -and ($_.UninstallString -or $_.NoRemove)} };
                #Check for 64bit Installed Apps and Pull Information
                if(Test-Path hklm:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall)
                {
                  Get-ChildItem hklm:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall `
                  | ForEach-Object { $installedAppsKeys += Get-ItemProperty $_.pspath `
                  | Where-Object {$_.DisplayName -and !$_.ReleaseType -and !$_.ParentKeyName `
                  -and ($_.UninstallString -or $_.NoRemove)} };
                }
                #Loop Through Installed Application Registry Keys and Pull Info
                foreach($instlApp in $installedAppsKeys)
                {
                    #Local Variables Used in Reporting
                    [string]$displayName = "";
                    [string]$displayVersion = "";
                    
                    #Check for DisplayName is Null or Emtpy
                    if(![string]::IsNullOrEmpty($instlApp.DisplayName))
                    {
                        $displayName = $instlApp.DisplayName.ToString();
                        #Check to See If Display Version is Null or Empty
                        if(![string]::IsNullOrEmpty($instlApp.DisplayVersion))
                        {
                           $displayVersion = $instlApp.DisplayVersion.ToString();
                        }
                        #Create Custom PSObject and Add to Reporting Array
                        $app = New-Object PSObject;
                        $app | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $displayName;
                        $app | Add-Member -MemberType NoteProperty -Name "DisplayVersion" -Value $displayVersion;
                        $installedApps += $app;
                    }
                 }
                #Send Back Reporting Array Sorted by Display Name
                $installedApps | Sort-Object DisplayName;                                                  
            }

$allComputers | Format-Table PSComputerName,DisplayName,DisplayVersion -AutoSize

PowerShell: WMI to Report Installed Applications on Remote Systems

Recently, I've been working with more PowerShell Remoting. Earlier this week I wrote a script that queries a list of systems for installed applications using Remoting. Tomorrow, I'm demo'ing that script (which I will post after this one) and needed to show how you would do it using just WMI in PowerShell. Since the listing of installed applications is stored in the Registery it took a good amount of time trying to figure out how to access a remote registry via PowerShell just using WMI. Below is the comparison script. Enjoy.


#########################################################
# Script Name: PS_Remote_WMI_Installed_Applications.ps1
# Version: 1.0
# Description: Using WMI Remotely Queries
#               Systems for Installed Software
#########################################################

#Array for Reporting Installed Software
$installedApps = @();

#Array for Registry Paths to Installed Apps
$appRegPaths = @("Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
                 "Software\Microsoft\Windows\CurrentVersion\Uninstall");

#Array of System Names to Run Against
$computers = @("SERVER01","SERVER02","SERVER03");

foreach($computer in $computers)
{
    #Ping System First
    if(Test-Connection -ComputerName $computer -Quiet)
    {
        #Connect to WMI Registry Class
        $uReg = [wmiclass]"\\$computer\root\default:StdRegProv";
                          
        foreach($regPath in $appRegPaths)
        {
            #Pull the Application Registry Keys 
            $iAppKeys = $uReg.EnumKey(2147483650,$regPath);
    
            #Null Check on Application Registry Keys
            if($iAppKeys)
            {
                #Loop Through Each Application Key
                foreach($appKey in $iAppKeys.sNames)
                {
                    #Construct Key Path
                    $keyPath = $regPath + "\" + $appKey.ToString();
                    
                    #Pull the Key DisplayName String Value
                    $keyDisplayName = $uReg.GetStringValue(2147483650,$keyPath,"DisplayName");
                    if(![string]::IsNullOrEmpty($keyDisplayName.sValue))
                    {
                        #Local Vars Used for Reporting
                        [string]$displayName = $keyDisplayName.sValue.ToString();
                        [string]$displayVersion = "";
                    
                        #Pull the Key DisplayVersion String Value
                        $keyDisplayVersion = $uReg.GetStringValue(2147483650,$keyPath,"DisplayVersion");
                        if(![string]::IsNullOrEmpty($keyDisplayVersion.sValue))
                        {
                            $displayVersion = $keyDisplayVersion.sValue.ToString();
                        }
                
                        #Create Custom PSObject and Add to Reporting Array
                        $app = New-Object PSObject;
                        $app | Add-Member -MemberType NoteProperty -Name "ComputerName" -Value $computer;
                        $app | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $displayName;
                        $app | Add-Member -MemberType NoteProperty -Name "DisplayVersion" -Value $displayVersion;
                          $installedApps += $app;
                        
                    }#End of Null\Empty Check on DisplayName String Value
                    
                }#End of Foreach $iAppKeys
            
            }#End of Null Check on $iAppKeys
        
        }#End of Foreach Reg Path
    
    }#End of Test-Connection

}#End of Foreach Computer

$installedApps | Sort-Object ComputerName,DisplayName | Format-Table -AutoSize;