Office Click-To-Run and XML Files

So, it used to be that we would install Office using a batch script that would invoke a setup.exe, assign a specific /configure flag and manually assign a specific XML file that contained the product that we wanted to install. This was bulky. It got too bulky when we needed to install 32-bit and 64-bit versions.

TIME FOR A CHANGE!

This is when I started thinking: “wow I really hate batch. I’m really glad I’m not the one that had to write this old script. Lets PowerShell this shit!”

First I needed a template XML file to modify, So this is what that looks like:

<Configuration>
  <Add OfficeClientEdition="64" Channel="Current">
    <Product ID="O365BusinessRetail">
      <Language ID="MatchOS" />
    </Product>
  </Add>
</Configuration>

This is the file that we will edit to say which product we want installing also if we want 64-bit or 32-bit.

Next, I needed to create a PowerShell script that would take a user’s input, edit the XML file accordingly and start the setup.exe with this flag. I also needed the bit-version that they wanted.

I started by defining the variables I would need for the script:

#Variables used for the installation
$bitVersion = ""
$officeProduct = ""
$pathToOffice = "\\path\to\office\folder"
$xmlFile = "OfficeXML.xml"
$pathToXMLFile = Join-Path -Path $pathToOffice -ChildPath $xmlFile

Then I created a function I would use to update the XML file. I needed two parameters, the product that they wanted installing and the bit version they wanted:

#Updates the XML file based on the input
function Update-XMLFile([string]$product, [string]$bit){

    try{
        #Loading the XML document
        [xml]$xmlDoc = Get-Content -Path $pathToXMLFile

        #Edit the document
        $xmlDoc.Configuration.Add.OfficeClientEdition = $bit
        $xmlDoc.Configuration.Add.Product.ID = $product

        #Save the document
        $xmlDoc.Save($pathToXMLFile)
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage -ForegroundColor Red
        Read-Host "The script encountered the above error - will now exit"
    }
}

I then created another function to start the installation. This also required two parameters, the bit version and the XML file name

#Function to start the installation
function Start-Installation([string]$bit, [string]$xmlName){
    try{
        .\setup.exe /configure $bit\$xmlName
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage
        Read-Host "The script encountered the above error - will now exit"
    }
}

My final function was a verification test. Since we want to only use 64-bit for future installations, I had to make sure that whoever was using the script knew this and would be competent enough to do a little bit of math:

#Function to check the user wants 32 bit
function Get-Verification(){
    $output = $false

    Write-Host "Are you sure you want to install 32-bit?" -ForegroundColor Red
    Write-Host "All new installs should use 64-bit instead"
    Write-Host "If you want to install 32-bit, complete the test below, otherwise enter the wrong answer"

    $firstNumber = Get-Random -Minimum 1 -Maximum 11
    $secondNumber = Get-Random -Minimum 1 -Maximum 11

    $sumToCheck = $firstNumber + $secondNumber

    $verificationInput = Read-Host "$($firstNumber) + $($secondNumber) = ?"

    if ($verificationInput -eq $sumToCheck){
        Write-Host "Fine! 32-bit will be installed..."
        $output = $true
    }else{
        Write-Host "Finally! 64-bit will be installed"
        $output = $false
    }
    return $output
}

Now that all my functions were defined, I could start with the actual meat of the script. This included cleaning the screen, asking the user some questions, launching the 32-bit verification is needed, updating the XML file using a switch statement and finally kicking off the installation. Heres what that looked like:

#Clear the screen
Clear-Host

#region Checking if the user wants 64 bit or 32 bit

do{

    Write-Host "Do you want" -NoNewline
    Write-Host " 64-bit " -NoNewline -ForegroundColor Yellow
    Write-Host "or" -NoNewline
    Write-Host " 32-bit " -NoNewline -ForegroundColor Green
    Write-Host "? (64 or 32): " -NoNewline
    $bitVersionInput = (Read-Host).ToUpper()
}while((64 ,32) -notcontains $bitVersionInput)

#endregion

#Check the user definitely wants 32 bit
if ($bitVersionInput -eq "32"){
    if (Get-Verification){
        $bitVersion = $bitVersionInput
    }else{
        $bitVersionInput = "64"
    }
}

#Update the bitVersion variable
$bitVersion = $bitVersionInput

#region Asking what product to install

#Ask the user what product they want to install
Write-Host @"

Please select one product from the below list

"@

Write-Host @"
1) Business Retail
2) ProPlus Retail

"@ -ForegroundColor Cyan

Write-Host @"
3) Visio Std Volume
4) Visio Pro Volume
5) Visio Pro Retail

"@ -ForegroundColor Green

Write-Host @"
6) Project Std Volume
7) Project Pro Volume
8) Project Pro Retail

"@ -ForegroundColor Gray

Write-Host @"
C) Cancel

"@ -ForegroundColor Red

do{
    $officeProductInput = (Read-Host "Enter a number").ToUpper()
}while((1,2,3,4,5,6,7,8, "C") -notcontains $officeProductInput)

#endregion

#Update the product variable
$officeProduct = $officeProductInput

#region Switch the input to see what it is and perform the required operation

switch($officeProduct){
    
    #Business Retail
    1 { Update-XMLFile -product "O365BusinessRetail" -bit $bitVersion}
    #ProPlus
    2 { Update-XMLFile -product "O365ProPlusRetail" -bit $bitVersion}
    #Visio Std Volume
    3 { Update-XMLFile -product "VisioStd2019Volume" -bit $bitVersion}
    #Visio Pro Volume
    4 { Update-XMLFile -product "VisioPro2019Volume" -bit $bitVersion}
    #Visio Pro Retail
    5 { Update-XMLFile -product "VisioPro2019Retail" -bit $bitVersion}
    #Project Std Volume
    6 { Update-XMLFile -product "ProjectStd2019Volume" -bit $bitVersion}
    #Project Pro Volume
    7 { Update-XMLFile -product "ProjectPro2019Volume" -bit $bitVersion}
    #Project Pro Retail
    8 { Update-XMLFile -product "ProjectPro2019Retail" -bit $bitVersion}
    #Cancel
    "C" {Exit}
    default {Exit}
}

#endregion

#Start the installation
Write-Host "Installing..." -ForegroundColor Green
Start-Installation -bit $bitVersion -xmlName $xmlFile
Write-Host "This window can be closed"
Read-Host

Done!

If you’re wondering what the script looks like as a whole, wonder no longer:

#Variables used for the installation
$bitVersion = ""
$officeProduct = ""
$pathToOffice = "\\sandpdc\software\Office"
$xmlFile = "OfficeXML.xml"
$pathToXMLFile = Join-Path -Path $pathToOffice -ChildPath $xmlFile

#Updates the XML file based on the input
function Update-XMLFile([string]$product, [string]$bit){

    try{
        #Loading the XML document
        [xml]$xmlDoc = Get-Content -Path $pathToXMLFile

        #Edit the document
        $xmlDoc.Configuration.Add.OfficeClientEdition = $bit
        $xmlDoc.Configuration.Add.Product.ID = $product

        #Save the document
        $xmlDoc.Save($pathToXMLFile)
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage -ForegroundColor Red
        Read-Host "The script encountered the above error - will now exit"
    }
}

#Function to start the installation
function Start-Installation([string]$bit, [string]$xmlName){
    try{
        .\setup.exe /configure $bit\$xmlName
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage
        Read-Host "The script encountered the above error - will now exit"
    }
}

#Function to check the user wants 32 bit
function Get-Verification(){
    $output = $false

    Write-Host "Are you sure you want to install 32-bit?" -ForegroundColor Red
    Write-Host "All new installs should use 64-bit instead"
    Write-Host "If you want to install 32-bit, complete the test below, otherwise enter the wrong answer"

    $firstNumber = Get-Random -Minimum 1 -Maximum 11
    $secondNumber = Get-Random -Minimum 1 -Maximum 11

    $sumToCheck = $firstNumber + $secondNumber

    $verificationInput = Read-Host "$($firstNumber) + $($secondNumber) = ?"

    if ($verificationInput -eq $sumToCheck){
        Write-Host "Fine! 32-bit will be installed..."
        $output = $true
    }else{
        Write-Host "Finally! 64-bit will be installed"
        $output = $false
    }
    return $output
}

#Clear the screen
Clear-Host

#region Checking if the user wants 64 bit or 32 bit

do{

    Write-Host "Do you want" -NoNewline
    Write-Host " 64-bit " -NoNewline -ForegroundColor Yellow
    Write-Host "or" -NoNewline
    Write-Host " 32-bit " -NoNewline -ForegroundColor Green
    Write-Host "? (64 or 32): " -NoNewline
    $bitVersionInput = (Read-Host).ToUpper()
}while((64 ,32) -notcontains $bitVersionInput)

#endregion

#Check the user definitely wants 32 bit
if ($bitVersionInput -eq "32"){
    if (Get-Verification){
        $bitVersion = $bitVersionInput
    }else{
        $bitVersionInput = "64"
    }
}

#Update the bitVersion variable
$bitVersion = $bitVersionInput

#region Asking what product to install

#Ask the user what product they want to install
Write-Host @"

Please select one product from the below list

"@

Write-Host @"
1) Business Retail
2) ProPlus Retail

"@ -ForegroundColor Cyan

Write-Host @"
3) Visio Std Volume
4) Visio Pro Volume
5) Visio Pro Retail

"@ -ForegroundColor Green

Write-Host @"
6) Project Std Volume
7) Project Pro Volume
8) Project Pro Retail

"@ -ForegroundColor Gray

Write-Host @"
C) Cancel

"@ -ForegroundColor Red

do{
    $officeProductInput = (Read-Host "Enter a number").ToUpper()
}while((1,2,3,4,5,6,7,8, "C") -notcontains $officeProductInput)

#endregion

#Update the product variable
$officeProduct = $officeProductInput

#region Switch the input to see what it is and perform the required operation

switch($officeProduct){
    
    #Business Retail
    1 { Update-XMLFile -product "O365BusinessRetail" -bit $bitVersion}
    #ProPlus
    2 { Update-XMLFile -product "O365ProPlusRetail" -bit $bitVersion}
    #Visio Std Volume
    3 { Update-XMLFile -product "VisioStd2019Volume" -bit $bitVersion}
    #Visio Pro Volume
    4 { Update-XMLFile -product "VisioPro2019Volume" -bit $bitVersion}
    #Visio Pro Retail
    5 { Update-XMLFile -product "VisioPro2019Retail" -bit $bitVersion}
    #Project Std Volume
    6 { Update-XMLFile -product "ProjectStd2019Volume" -bit $bitVersion}
    #Project Pro Volume
    7 { Update-XMLFile -product "ProjectPro2019Volume" -bit $bitVersion}
    #Project Pro Retail
    8 { Update-XMLFile -product "ProjectPro2019Retail" -bit $bitVersion}
    #Cancel
    "C" {Exit}
    default {Exit}
}

#endregion

#Start the installation
Write-Host "Installing..." -ForegroundColor Green
Start-Installation -bit $bitVersion -xmlName $xmlFile
Write-Host "This window can be closed"
Read-Host

 

Quering and Adding Info To Access Database Using C#

In this post, I will show you how I created a program to extract and add data to an Access database. Before we get started, you can see my current specifications below:

Getting values from a table:

Using System.Data.OleDB;

//Create a new list to hold all the values
List<String> values = new List<String>();

//Build the connection string and SQL string
string connectionString = @$"Provider=Microsoft.ACE.OLEDB;Data Source = C:\Path\To\Access.accdb";
string sqlString = "SELECT * FROM Table_Name";

//Create a new connection to the Access file
using (OleDbConnection connection = new OleDbConnection(connectionString)){

    //Creating a new command
    OleDbCommand command = new OleDbCommand(sqlString, connection);
    
    //Try/catch to catch errors, DON'T DO THIS IN SERIOUS PROJECTS!
    try{
        
        //Opening the connection and reading the data
        connection.Open();
        using(OleDbDataReader reader = command.ExecuteReader()){
            while(reader.Read()){
                
                //Adding the value to the values list
                values.Add(reader["Field_Name"].ToString());
            }
        }
    }catch{ }
    
    //Closing the connection
    connection.Close();
}

//Sorting the list in ascending order
values.Sort();

 

Adding a new row to the table:

Using System.Data.OleDb;

//Building the connection string and SQL string
string connectionString = @$"Provider=Microsoft.ACE.OLEDB.12.0;Data Source = C:\Path\To\Access.accdb";
string sqlString = $"INSERT INTO Table_Name(Field_Name1, Field_Name2) VALUES ('{Field_Value1}','{Field_Value2}')";

//Creating a new connection to the Access file
using (OleDbConnection connection = new OleDbConnection(connectionString))
{
    //Build a new command
    using(OleDbCommand command = new OleDbCommand(sqlString, connection))
    {
        //Open the database connection and execute the write
        connection.Open();
        command.ExecuteReader();
    }
    //Close the database connection
    connection.Close();
}

Enjoy!

LAPS 3

Okay, this is probably my final update to the whole LAPS thing. I have created two iterations in the past but neither were really groundbreaking or my own design. Not that this update is groundbreaking either though. This is a further update to the below post:

LAPS WinForm 2

I wanted to completely redo my LAPS form (again) to make it my own design, responsive and ultimately better. This is what the final form looks like. It is completely responsive and resizeable:

I will include the source code here but the best place to download this would be from my TechNet gallery.

There are a couple of things you need to change in the form to make it work:

  • Adding your domain controller and domain root to the variables at the top of the script
  • Add your BASE64 data into the BASE64 variable to use your own logo

Heres the code:

#Enter your domain and domain controller below :)
$script:domainController = "DOMAIN CONTROLLER HERE" #E.G domaincontroller.domain.lan
$script:domainRoot = "DOMAIN ROOT HERE" #E.G domain.lan

#LOADING ASSEMBLIES
Add-Type -AssemblyName PresentationFramework, System.Drawing, System.Windows.Forms, WindowsFormsIntegration

#ICON FOR FORM
[string]$base64=@'
BASE64 DATA HERE
'@

#CREATING THE IMAGE FROM BASE64 DATA
$bitmap = New-Object System.Windows.Media.Imaging.BitMapImage
$bitmap.BeginInit()
$bitmap.StreamSource = [System.IO.MemoryStream][System.Convert]::FromBase64String($base64)
$bitmap.EndInit()
$bitmap.Freeze()

#LAPS WINDOW XML
[xml]$LAPSXaml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="LAPS UI" Height="400" Width="400" MinHeight="400" MinWidth="400" WindowStartupLocation="CenterScreen">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2"/>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto" MinWidth="75"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto" MinHeight="7"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Label Content="ComputerName:" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Column="1" FontSize="14"/>
        <TextBox Name="Computer_Textbox" VerticalContentAlignment="Center" HorizontalAlignment="Stretch" Grid.Row="1" TextWrapping="Wrap" VerticalAlignment="Stretch" Margin="3" Grid.Column="1" FontSize="14"/>
        <Button Name="Search_Button" Content="Search" Grid.Column="2" HorizontalAlignment="Stretch" Grid.Row="1" VerticalAlignment="Stretch" Margin="0,3,5,3"/>
        <Label Content="Password" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="2" VerticalAlignment="Stretch" FontSize="14"/>
        <TextBox Name="Password_Textbox" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="3" TextWrapping="Wrap" Margin="3" VerticalAlignment="Stretch" IsReadOnly="True" FontSize="14"/>
        <Button Name="Copy_Button" Content="Copy" Grid.Column="2" HorizontalAlignment="Stretch" Grid.Row="3" Margin="0,3,5,3" VerticalAlignment="Stretch"/>
        <Label Content="Password Expires" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="4" VerticalAlignment="Stretch" FontSize="14"/>
        <TextBox Name="Password_Ex_Textbox" Grid.Column="1" IsReadOnly="True" HorizontalAlignment="Stretch" Grid.Row="5" TextWrapping="Wrap" VerticalAlignment="Stretch" Margin="3" FontSize="14"/>
        <Label Content="New Expiration" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="6" VerticalAlignment="Stretch" FontSize="14"/>
        <DatePicker Name="Date_Picker" Grid.Column="1" HorizontalAlignment="Stretch" Grid.Row="7" VerticalAlignment="Stretch" Margin="3" FontSize="14"/>
        <Button Name="Set_Button" Content="Set" Grid.Column="2" HorizontalAlignment="Stretch" Grid.Row="7" VerticalAlignment="Stretch" Margin="0,5,5,5"/>
        <GridSplitter IsEnabled="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="8" Grid.Column="1" Margin="5,2,5,2" Grid.ColumnSpan="2"/>
        <TextBox Name="Output_Textbox" VerticalScrollBarVisibility="Auto" IsReadOnly="True" HorizontalAlignment="Stretch" Grid.Row="9" TextWrapping="Wrap" Margin="1,5,1,1" VerticalAlignment="Stretch" Grid.ColumnSpan="3" FontSize="12"/>
    </Grid>
</Window>
"@

#LOADING XAML
$LAPSReader=(New-Object System.Xml.XmlNodeReader $LAPSXaml)
$LAPSWindow=[Windows.Markup.XamlReader]::Load($LAPSReader)
$LAPSWindow.Icon = $bitmap

#ASSIGNING CONTROLS
$Computer_Textbox = $LAPSWindow.FindName("Computer_Textbox")
$Search_Button = $LAPSWindow.FindName("Search_Button")
$Password_Textbox = $LAPSWindow.FindName("Password_Textbox")
$Copy_Button = $LAPSWindow.FindName("Copy_Button")
$Password_Ex_Textbox = $LAPSWindow.FindName("Password_Ex_Textbox")
$Date_Picker = $LAPSWindow.FindName("Date_Picker")
$Set_Button = $LAPSWindow.FindName("Set_Button")
$Output_Textbox = $LAPSWindow.FindName("Output_Textbox")

#FUNCTION TO SET OUTPUT TEXTBOX
function set-output-textbox{
    param(
        [string]$value,
        [bool]$date
    )
    if ($date){
        $Output_Textbox.Text = ("[$(Get-Date)] - $value `r`n")
    }else{
        $Output_Textbox.Text = $value
    }
}

#FUNCTION TO UPDATE OUTPUT TEXTBOX
function update-output-textbox{
    param(
        [string]$value,
        [bool]$date
    )
    if ($date){
        $Output_Textbox.AppendText("[$(Get-Date)] - $value `r`n")
    }else{
        $Output_Textbox.AppendText("     $value `r`n")
    }
    $Output_Textbox.ScrollToEnd()
}

#FUNCTION TO UPDATE FORM
function update-form{
    [System.Windows.Forms.Application]::DoEvents()
}

#FUNCTION TO UPDATE PASSWORD TEXTBOX
function update-password-textbox($value){
    $Password_Textbox.Text = $value
}

#FUNCTION TO UPDATE PASSWORD EX TEXTBOX
function update-passwordex-texbox($value){
    $Password_Ex_Textbox.Text = $value
}

#FUNCTION TO SET CONTROLS
function set-controls{
    param(
        [bool]$switcher,
        [bool]$setswitcher
    )
    $Search_Button.IsEnabled = $switcher
    $Set_Button.IsEnabled = $setswitcher
    $Date_Picker.IsEnabled = $setswitcher
}

#DECIDE IF COPY BUTTON SHOULD BE ENABLED
$Copy_Button.IsEnabled = $false
$Password_Textbox.Add_TextChanged({
    if ($Password_Textbox.Text.Length -gt 0){
        $Copy_Button.IsEnabled = $true
    }else{
        $Copy_Button.IsEnabled = $false
    }
})

#MAKING COMPUTER NAME UPPERCASE ON FOCUS LOST
$Computer_Textbox.Add_LostFocus({
    $Computer_Textbox.Text = $Computer_Textbox.Text.ToUpper()
})

#COPY BUTTON LOGIC
$Copy_Button.Add_Click({
    Set-Clipboard -Value $Password_Textbox.Text
})

#COMPUTER TEXTBOX KEYDOWN LOGIC
$Computer_Textbox.Add_KeyDown({
    if ($args.Key -eq 'Enter'){
        $Search_Button.RaiseEvent((New-Object -TypeName System.Windows.RoutedEventArgs $([System.Windows.Controls.Button]::ClickEvent)))
    }
})

#DISABLING CONTROLS ON FORM LOAD
set-controls -switcher $true -setswitcher $false

#WELCOME MESSAGE ON FORM LOAD
$Output_Textbox.HorizontalContentAlignment="Center"
$Output_Textbox.VerticalContentAlignment="Center"
set-output-textbox -date $false -value "Welcome to version 3 of this form! It is now responsive and a lot cleaner in the background. Nothing you ever had to worry about though :)"

#SEARCH BUTTON LOGIC
$Search_Button.Add_Click({

    #DISABLING CONTROLS ON BUTTON PRESS
    $Output_Textbox.HorizontalContentAlignment="Left"
    $Output_Textbox.VerticalContentAlignment="Top"
    set-controls -switcher $false -setswitcher $false
    update-password-textbox -value $null
    update-passwordex-texbox -value $null
    $Date_Picker.Text = $null

    if ($Computer_Textbox.Text.Length -le 0){
        #OUTPUT IF EMPTY SEARCH AND ENABLING CONTROLS
        set-output-textbox -date $true -value "Input cannot be empty"
        set-controls -switcher $true -setswitcher $false    
    }else{
        set-output-textbox -date $true -value "Please Wait"
        
        #PUTTING INPUT INTO VARIABLE
        $script:computerName = $Computer_Textbox.Text

        #CREATING A SYNCHRONISED HASHTABLE
        $script:syncHash = [hashtable]::Synchronized(@{})

        #CREATING SEARCH RUNSPACE
        $searchRunspace = [runspacefactory]::CreateRunspace()
        $searchRunspace.ApartmentState = "STA"
        $searchRunspace.ThreadOptions = "ReuseThread"
        $searchRunspace.Open()
        $searchRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
        $searchRunspace.SessionStateProxy.SetVariable("computerName",$computerName)
        $searchRunspace.SessionStateProxy.SetVariable("domainController",$domainController)

        #POWERSHELL TO BE RAN IN RUNSPACE
        $searchPowerShell = [powershell]::Create().AddScript({
            $syncHash.searchADComputer = Get-ADComputer -Identity $computerName
            $syncHash.searchInvoke = Invoke-Command -ComputerName $domainController -ScriptBlock { Get-AdmPwdPassword -ComputerName $args[0] } -ArgumentList $computerName | Select-Object Password, ExpirationTimeStamp
        })

        #ASSIGNING RUNSPACE TO POWERSHELL
        $searchPowerShell.Runspace = $searchRunspace
        #STARTING THE RUNSPACE AND POWERSHELL
        $searchObject = $searchPowerShell.BeginInvoke()

        #REFRESHING UNTIL POWERSHELL IS COMPLETE
        do{
            Start-Sleep -Milliseconds 100
            update-form
        }while (!$searchObject.IsCompleted)

        #ENDING POWERSHELL INVOKE AND DISPOSING OF RUNSPACE
        $searchPowerShell.EndInvoke($searchObject)
        $searchPowerShell.Dispose()
    
        if ($syncHash.searchADComputer){
            #COMPUTER IS FOUND ON DOMAIN
            if ($syncHash.searchInvoke){
                #INVOKE SUCCESSFUL
                $admpwdPassword = $syncHash.searchInvoke.password
                $admpwdPasswordExpiration = $syncHash.searchInvoke.ExpirationTimeStamp
                $admpwdPasswordExpirationFormatted = $admpwdPasswordExpiration.ToString("dd/MM/yyyy hh:mm:ss")

                #UPDATING FIELDS
                update-output-textbox -date $true -value "Information retrieved"
                update-password-textbox -value $admpwdPassword
                update-passwordex-texbox -value $admpwdPasswordExpirationFormatted
                set-controls -switcher $true -setswitcher $true
            }else{
                #INVOKE FAILED
                update-output-textbox -date $true -value "Failded to retrieve password information"
                update-password-textbox -value $null
                update-passwordex-texbox -value $null
                set-controls -switcher $true -setswitcher $false
            }
        }else{
            #COMPUTER NOT FOUND ON DOMAIN
            update-output-textbox -date $true -value "Host not found on domain"
            update-password-textbox -value $null
            update-passwordex-texbox -value $null
            set-controls -switcher $true -setswitcher $false
        }
    }
})

#SET EXPIRATION BUTTON LOGIC
$Set_Button.Add_Click({
    
    #DISABLING CONTROLS ON BUTTON PRESS
    set-controls -switcher $false -setswitcher $false

    if ($Date_Picker.Text.Length -le 0){
        #OUTPUT IF EMPTY DATE AND ENABLING CONTROLS
        update-output-textbox -date $true -value "No date selected"
        set-controls -switcher $true -setswitcher $true
    }else{
        #GETTING NEW DATES FOR EXPIRATION
        $newExpirationString = $Date_Picker.SelectedDate.ToString("MM/dd/yyyy")
        $script:newExpirationDate = [datetime]::ParseExact($newExpirationString, 'MM/dd/yyyy', $null)
        
        #OUTPUTTING FRIENDLY EXPIRATION TO OUTPUT TEXTBOX
        update-output-textbox -date $true -value "Setting expiration to $newExpirationString..."

        #CREATING SEARCH RUNSPACE
        $setRunspace = [runspacefactory]::CreateRunspace()
        $setRunspace.ApartmentState = "STA"
        $setRunspace.ThreadOptions = "ReuseThread"
        $setRunspace.Open()
        $setRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
        $setRunspace.SessionStateProxy.SetVariable("computerName",$computerName)
        $setRunspace.SessionStateProxy.SetVariable("domainController",$domainController)
        $setRunspace.SessionStateProxy.SetVariable("newExpirationDate",$newExpirationDate)

        #POWERSHELL TO BE RAN IN RUNSPACE
        $setPowerShell = [powershell]::Create().AddScript({
            try{
                $syncHash.setInvoke = Invoke-Command -ComputerName $domainController -ScriptBlock {Reset-AdmPwdPassword -ComputerName $args[0] -WhenEffective $args[1] } -ArgumentList $computerName, $newExpirationDate -ErrorAction Stop
                try{
                    Invoke-GPUpdate -Computer $computerName -ErrorAction Stop
                    $syncHash.setGPUpdate = $true
                }catch{
                    #GP UPDATE FAILED
                    $syncHash.setGPUpdate = $null
                }
            }catch{
                #CHANGING EXPIRATION FAILED
                $syncHash.setInvoke = $null
            }
        })

        #ASSIGNING RUNSPACE TO POWERSHELL
        $setPowerShell.Runspace = $setRunspace
        #STARTING THE RUNSPACE AND POWERSHELL
        $setObject = $setPowerShell.BeginInvoke()

        #REFRESHING UNTIL POWERSHELL IS COMPLETE
        do{
            Start-Sleep -Milliseconds 100
            update-form
        }while (!$setObject.IsCompleted)

        #ENDING POWERSHELL INVOKE AND DISPOSING OF RUNSPACE
        $setPowerShell.EndInvoke($setObject)
        $setPowerShell.Dispose()

        #CHECKING PASSWORD EXPIRATION SUCCESS
        if ($syncHash.setInvoke){
            update-output-textbox -date $true -value "Successfully reset password expiration date"
            #CHECKING GP UPDATE SUCCESS
            if ($syncHash.setGPUpdate){
                update-output-textbox -date $true -value "Succesfully ran GP update"
            }else{
                update-output-textbox -date $true -value "Failed to run GP update, this is probably due to permissions"
            }
        }else{
            update-output-textbox -date $true -value "Failed to reset password expiration date"
        }

        #RESETTING CONTROLS
        set-controls -switcher $true -setswitcher $true
    }
})

#CHECK FOR AD MODULE AND TEST IF ON LOCAL DOMAIN/NETWORK
if ( Test-Connection $domainRoot -Count 1 -Quiet){
    #DOMAIN IS ACCESSIBLE
    if (Get-Module -List ActiveDirectory ){
        #AD MODULE INSTALLED
        #FORM WILL BE DISPLAYED WITHOUT ANY MODIFICATIONS
    }else{
        #AD MODULE NOT INSTALLED
        set-output-textbox -date $false -value "Install the AD module and restart"
        set-controls -switcher $false -setswitcher $false
        $Computer_Textbox.IsEnabled = $false
    }
}else{
    #DOMAIN ISN'T ACCESSIBLE
    set-output-textbox -date $false -value "$domainRoot is not accessible"
    set-controls -switcher $false -setswitcher $false
    $Computer_Textbox.IsEnabled = $false
}   

#REMOVING PROCESS ON FORM CLOSE
$LAPSWindow.Add_Closing({
    try{
        $syncHash.Clear() | Out-Null
    }catch{}
    
    Stop-Process -Name "LAPS" -ErrorAction SilentlyContinue
})

#DISPLAY FORM WHILST TESTING
$app = [Windows.Application]::new()
$app.run($LAPSWindow)

Enjoy!

Responsive PowerShell WPF Form Introduction #2

Following on from my last post, I’m going to show you how to update a textbox using a button on the same form. I will be adding the following code starting on line 38:

#BUTTON LOGIC
$syncHash.Button.Add_Click({

    $syncHash.Window.Dispatcher.Invoke(
        [action]{
            $syncHash.TextBox.AppendText("This is a test")
        }
    )
})

This is fairly basic in what it does. It just adds “This is a test” to the textbox. Say if I want the button to run a task and then update the textbox with the results, but the results took a long time to come, the form would freeze. This is because whatever command you run in the same runspace as the GUI, takes controls and stops the GUI being responsive.

So, what I’m going to do is ping google 5 times, get the average from all of those and then update the textbox without the GUI becoming unresponsive. To do this, I’m going to create a new runspace and add the code I want to run. You can see this below:

#BUTTON LOGIC
$syncHash.Button.Add_Click({
    #ASSIGNING HOST VARIABLE
    $syncHash.host = $Host
    #CREATING NEW RUNSPACE
    $pingrunspace = [runspacefactory]::CreateRunspace()
    $pingrunspace.ApartmentState = "STA"
    $pingrunspace.ThreadOptions = "ReuseThread"
    $pingrunspace.Open()
    #PUTTING THE SYNCHASH VARIABLE INSIDE THE NEW RUNSPACE
    $pingrunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)

    #THIS IS THE CODE THAT WILL BE EXECUTED IN THE NEW RUNSPACE
    $code = {

        #CONNECTION TO GOOGLE AND CALCULATING AVERAGE IN NEW RUNSPACE
        $connection = Test-Connection -ComputerName google.co.uk -Count 5
        $average = [math]::Round(($connection.responsetime | Measure-Object -Average).Average)
        #UPDATING THE TEXTBOX WITH CONNECTION AVERAGE IN NEW RUNSPACE
        $syncHash.Window.Dispatcher.Invoke(
            [action]{
                $syncHash.TextBox.AppendText($average)
            }
        )

    }
        
    #ADDING AND RUNNING THE CODE IN THE NEW RUNSPACE
    $PSInstance = [powershell]::Create().AddScript($code)
    $PSinstance.Runspace = $pingrunspace
    $job = $PSinstance.BeginInvoke()
    
})

This will run the code in a separate runspace to the GUI and allow you to interact with it whilst the commands complete in the background.

Just in case you want the entire this, this is what the whole file looks like 🙂

#CREATE HASHTABLE AND RUNSPACE FOR GUI
$syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"         
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)      
#BUILD GUI AND ADD TO RUNSPACE CODE
$psCmd = [PowerShell]::Create().AddScript({   
    [xml]$xaml = @"
    <Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Name="Window" Height="400" Width="600">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <Button Name="Button" Content="Press" Height="200" Width="580" Grid.Row="0" Grid.Column="0" />
        <TextBox Name="Textbox" Height="200" Width="580" Grid.Row="1" Grid.Column="0" />
    </Grid>
</Window>
"@
  
    #INTERPRET AND LOAD THE GUI
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )

    #EXTRACT THE CONTROLS FROM THE GUI
    $syncHash.TextBox = $syncHash.window.FindName("Textbox")
    $syncHash.Button = $syncHash.Window.FindName("Button")

    #BUTTON LOGIC
    $syncHash.Button.Add_Click({

        $syncHash.host = $Host
        $pingrunspace = [runspacefactory]::CreateRunspace()
        $pingrunspace.ApartmentState = "STA"
        $pingrunspace.ThreadOptions = "ReuseThread"
        $pingrunspace.Open()
        $pingrunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)

        $code = {

            $connection = Test-Connection -ComputerName google.co.uk -Count 5
            $average = [math]::Round(($connection.responsetime | Measure-Object -Average).Average)
            $syncHash.Window.Dispatcher.Invoke(
                [action]{
                    $syncHash.TextBox.AppendText($average)
                }
            )

        }

        $PSInstance = [powershell]::Create().AddScript($code)
        $PSinstance.Runspace = $pingrunspace
        $job = $PSinstance.BeginInvoke()
    })


    #FINALISE AND CLOSE GUI RUNSPACE UPON EXITING
    $syncHash.Window.ShowDialog() | Out-Null
    $syncHash.Error = $Error
    $Runspace.Close()
    $Runspace.Dispose()
    
})
#LOAD RUNSPACE WITH GUI IN
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

Enjoy!

LAPS WinForm 2

New and improved LAPS WinForm because the original one, found here, was kind of crap. It didn’t handle exceptions very well and I don’t think the group policy update worked at all after some further debugging.

I am please to present the new GUI for LAPS:

The best place to download this from would be my TechNet gallery

Enjoy!

SharePoint Group Membership WinForm

This is a little WinForm I created that would output the group membership for a domain user or FBA (Forms-Based Authentication) user on SharePoint.

This is what the form looks like, it gives the option for a domain or FBA user and also checked if the user exists before trying to get the relevant information:

The form first checks if CredSSP is configured on your machine to delegate your credentials to the SharePoint server. The form then loads, waits for your input, validates your input and finally collects the group information for your input.

And finally, this is the code for the Winform. I’ve removed some details as they need to be filled in by you. Enjoy!

#CHECKING CREDSSP SETTINGS
if ((Get-Item  WSMan:\localhost\Client\Auth\CredSSP).value -eq $false){
    #CREDSSP NOT CONFIGURED, EXITING
    Write-Host @"
    
CredSSP is not configured!

Please open an elavated PowerShell prompt and run:

Enable-WSManCredSSP -Role client -DelegateComputer sandsharepointf

"@
    Exit
}else{}

#LOADING ASSEMBLIES
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()

#ICON FOR THE FORM
[string]$icon64=@"
#base64data
"@

#CONVERTING BASE 64 ICON INTO GRAPHIC
$iconstream = [System.IO.MemoryStream][System.Convert]::FromBase64String($icon64)
$iconbmp = [System.Drawing.Bitmap][System.Drawing.Image]::FromStream($iconstream)
$iconhandle = $iconbmp.GetHicon()
$icon = [System.Drawing.Icon]::FromHandle($iconhandle)

#FORM
$SharePoint_Membership_Form                 = New-Object system.Windows.Forms.Form
$SharePoint_Membership_Form.ClientSize      = '400,278'
$SharePoint_Membership_Form.text            = "SharePoint Membership"
$SharePoint_Membership_Form.TopMost         = $false
$SharePoint_Membership_Form.StartPosition   = "centerscreen"
$SharePoint_Membership_Form.Icon            = $icon
$SharePoint_Membership_Form.FormBorderStyle = "Fixed3D"

#USERNAME LABEL
$Username_Label                  = New-Object system.Windows.Forms.Label
$Username_Label.text             = "Enter a username:"
$Username_Label.AutoSize         = $true
$Username_Label.width            = 25
$Username_Label.height           = 10
$Username_Label.location         = New-Object System.Drawing.Point(146,12)

#USERNAME TEXTBOX
$Username_Textbox                = New-Object system.Windows.Forms.TextBox
$Username_Textbox.multiline      = $false
$Username_Textbox.width          = 175
$Username_Textbox.height         = 20
$Username_Textbox.location       = New-Object System.Drawing.Point(115,33)

#VARIABLE FOR KEYDOWN
$Username_Textbox_keydown = {}

#KEYDOWN ASSIGNED
$Username_Textbox_keydown = [System.Windows.Forms.KeyEventHandler]{
    if ($_.keycode -eq 'Enter'){
        $Search_Button.PerformClick()
    }
}

#REGISTER KEYDOWN HANDLER TO USERNAME TEXTBOX
$Username_Textbox.add_keydown($Username_Textbox_keydown)

#DOMAIN RADIO BUTTON
$Domain_User_RB                  = New-Object system.Windows.Forms.RadioButton
$Domain_User_RB.text             = "Domain User"
$Domain_User_RB.AutoSize         = $true
$Domain_User_RB.width            = 104
$Domain_User_RB.height           = 20
$Domain_User_RB.location         = New-Object System.Drawing.Point(120,64)
$Domain_User_RB.Checked          = $true

#SHAREPOINT FBA USER RADIO BUTTON
$FBA_User_RB                     = New-Object system.Windows.Forms.RadioButton
$FBA_User_RB.text                = "FBA User"
$FBA_User_RB.AutoSize            = $true
$FBA_User_RB.width               = 104
$FBA_User_RB.height              = 20
$FBA_User_RB.location            = New-Object System.Drawing.Point(215,64)

#SEARCH BUTTON
$Search_Button                  = New-Object system.Windows.Forms.Button
$Search_Button.text             = "Search"
$Search_Button.width            = 60
$Search_Button.height           = 30
$Search_Button.location         = New-Object System.Drawing.Point(171,89)

#SEPERATOR LINE
$Seperator_Label                 = New-Object system.Windows.Forms.Label
$Seperator_Label.text            = ""
$Seperator_Label.AutoSize        = $false
$Seperator_Label.BorderStyle     = "Fixed3D"
$Seperator_Label.width           = 390
$Seperator_Label.height          = 2
$Seperator_Label.location        = New-Object System.Drawing.Point(5,124)

#OUTPUT TEXTBOX
$Output_Textbox                 = New-Object System.Windows.Forms.TextBox
$Output_Textbox.Multiline       = $true
$Output_Textbox.Width           = 390
$Output_Textbox.Height          = 142
$Output_Textbox.Location        = New-Object System.Drawing.Point(5,131)
$Output_Textbox.ReadOnly        = $true
$Output_Textbox.ScrollBars      = "vertical"

#ADDING CONTROLS TO FORM
$SharePoint_Membership_Form.controls.AddRange(@($Domain_User_RB,$FBA_User_RB,$Seperator_Label,$Username_Label,$Username_Textbox,$Search_Button,$Output_Textbox))

$Search_Button.add_click({

    $Output_Textbox.Text = ""

    #DATE FOR OUTPUT
    $date = Get-Date    
    $username_value = $Username_Textbox.Text
    $Username_Prefix = $null
    $location = #base location

    #SETTING SEARCH VALUES BACK TO FALSE
    $Search_On_AD_User = $false
    $Search_On_FBA_User = $false

    #CHECKING IF USERNAME TEXTBOX IS EMPTY
    if ($Username_Textbox.Text.Length -le 0){
        #IF EMPTY, VARIABLE IS FALSE
        $Output_Textbox.AppendText("$date - $Username cannot be empty! `n")
        $Username_Not_Empty = $false
    }else{
        $Username_Not_Empty = $true
        $Output_Textbox.Text = ""
    }

    #RUNS IF DOMAIN USER RADIO BUTTON IS CHECKED
    if ($Domain_User_RB.Checked -and $Username_Not_Empty){
        try{
            $Output_Textbox.AppendText("$date - Searching for $username_value `n")
            Get-ADUser -Identity $username_value

            $Output_Textbox.AppendText("$date - Found user! `n")

            $Search_On_AD_User = $true
            $Search_On_FBA_User = $false

            $Username_Found = $true

        }catch{
            $Output_Textbox.AppendText("$date - Cannot find domain user `n")
            $Username_Found = $false
        }
    }

    #RUNS IF FBA USER RADIO BUTTON IS CHECKED
    if ($FBA_User_RB.Checked -and $Username_Not_Empty){
        
        $SPAdmin = "sharepoint_admin_user"
        $credential = New-Object System.Management.Automation.PSCredential $SPAdmin,  (Get-Content "$location\sharepoint_admin_user_encrypted_password.txt" | ConvertTo-SecureString )

        $sb = {
            $username = $args[0]
            Add-PSSnapin microsoft.sharepoint.PowerShell
            $user = Get-SPUser -Limit All -Web http://SHAREPOINTSERVER | 
                Where-Object {$_.loginname -like "i:0#.f|fbamembershipprovider|$username"}

            return $user
        }

        $Output_Textbox.AppendText("$date - Trying to find $username_value... `n")

        $invokeoutputfbasearch = Invoke-Command -ScriptBlock $sb -ComputerName SHAREPOINTSERVER -Authentication Credssp -Credential $credential -ArgumentList $username_value

        if ($invokeoutputfbasearch){
            #FOUND USER
            $Username_Found = $true
            $Search_On_FBA_User = $true
            $Search_On_AD_User = $false
            $Output_Textbox.AppendText("$date - Found FBA user!`n")
        }else{
            #NOT FOUND USER
            $Username_Found = $false
            $Output_Textbox.AppendText("$date - Cannot find FBA user `n")
        }
    }

    #ONLY RUNS IF BELOW CONDITIONS ARE MET
    if ($Username_Found -and $Username_Not_Empty){
        #ASSIGNING THE RIGHT USERNAME FORMAT
        if ($Search_On_AD_User){
            $Username_Prefix = "*|DOMAIN_NAME\"
        }else{
            $Username_Prefix = "i:0#.f|fbamembershipprovider|"
        }

        $SPAdmin = "sharepoint_admin_user"
        $credential = New-Object System.Management.Automation.PSCredential $SPAdmin,  (Get-Content "$location\sharepoint_admin_user_encrypted_password.txt" | ConvertTo-SecureString )

        $sb = { 
            $groups = $null
            $prefix = $args[0]
            $username = $args[1]
            Add-PSSnapin Microsoft.SharePoint.PowerShell
            $user = get-SPUser -limit all -web http://SHAREPOINTSERVER | 
                Where-Object { $_.loginname -like "$prefix$username" }
                $SPGroups = get-spsite -limit all | 
                    Select-Object -ExpandProperty rootweb | 
                    Select-Object -ExpandProperty siteusers | 
                    Where-Object { $user.userlogin -eq $_.loginname } | 
                    Select-Object -ExpandProperty groups | 
                    Select-Object -ExpandProperty name
            foreach ($i in $SPGroups){
                $groups = $groups + "     - $i `r`n"
            }
        return $groups
        }

        $Output_Textbox.AppendText("$date - Collecting group info on $username_value... `n")

        $InvokeOutputfinal = Invoke-Command -ScriptBlock $sb -ComputerName SHAREPOINTSERVER -Authentication Credssp -Credential $credential -ArgumentList $Username_Prefix,$username_value

        $Output_Textbox.AppendText("`n")
        $Output_Textbox.AppendText("$InvokeOutputfinal")

    }else{#THIS SERVES ONLY AS A TRAP TO STOP ANYTHING RUNNING
    }
})

#DISPLAYING FORM
[void]$SharePoint_Membership_Form.ShowDialog()

Using DinoPass in PowerShell

This is a nice little trick I learnt whilst automating domain user creation with PowerShell, I found generating passwords in PowerShell was always ugly. Just see the example below from a previous post I’d made:

[string]$initialpassword = ([char[]](Get-Random -input $(47..57 + 65..90 +97..122) -count 8)) + (Get-Random -minimum 0 -maximum 10)

$passwordwithspacesremoved = $initialpassword.Replace(' ','')

$convertedpassword = ConvertTo-SecureString -AsPlainText $passwordwithspacesremoved -Force

This would generate a password like “cDUtxlvM5” which is just about as ugly as the code used to create it.

So I decided to use DinoPass instead since it created better looking passwords without the faff of generating them in PowerShell. This is a the code I used:

Invoke-WebRequest -Uri https://www.dinopass.com/password/strong | Select-Object -ExpandProperty content

Which would give me a much nicer, but still secure, password like “poorJump62”. Then to use it when automating domain user creation, I would use the below and put the whole thing into a variable that I would set the password to:

$super_secure_password = Invoke-WebRequest -Uri https://www.dinopass.com/password/strong | Select-Object -ExpandProperty content | ConvertTo-SecureString -AsPlainText -Force

Enjoy!

Server Reboot Script

Running a little low on content this last few months, plus I’ve been busy with other work stuff.

I had the requirement to create a PowerShell script that would get the uptime of a server and then decide whether or not the server needed rebooting.

I also wanted the script to randomize the reboot of the servers, that way if there are multiple servers that need rebooting at once, they don’t cause a power spike or resource issues on the hosts. I did this by creating a random number between 1 and 5 and then if the number equals 5, the server is rebooted. If not then the server isn’t rebooted.

This is the script that I ended up with and what is currently being tested:

$loglocation = "C:\scripts\reboot\log"
$dateforfile = Get-Date

#GETS UPTIME IN DAYS
$lastbootuptime = Get-WmiObject win32_operatingsystem
$uptime = (Get-Date) - ($lastbootuptime.converttodatetime($lastbootuptime.lastbootuptime))
$uptimeindays = $uptime.days

#GETS RANDOM NUMBER
$randomnumber = Get-Random -Minimum 1 -Maximum 6

if ($uptimeindays -ge "14"){

 Add-Content -Path "$loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
Server restarted at:
$dateforfile
This was an immediate shutdown as the server had been up for $uptimeindays days
"@

 Restart-Computer -Force

}elseif ($uptimeindays -lt "14" -and $uptimeindays -ge "7"){

    if ($randomnumber -eq "5"){

        Add-Content -Path "$loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
Server restarted at :
$dateforfile
This was a random restart as uptime was only $uptimeindays days
"@
        Restart-Computer -Force
    }else{

        Add-Content -Path "$loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
Server NOT restarted
$dateforfile
This was not randomly restarted. Uptime is currently $uptimeindays days. Random number was $randomnumber
"@
    }
}else{

Add-Content -Path "loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
No restart required
$dateforfile
No restart required since uptime is only $uptimeindays days
"@
}

The first time I created this script and set it up as a scheduled task, nothing happened. Turns out that I needed the -Force parameter in order for the server to be rebooted.

This will later be used in a group policy without the log creating as that is only necessary in the testing stage.

Enjoy!

 

Creating a Logon Script To Cleanup User Directories

In this post, I’ll discuss how I created a PowerShell script that runs when a user logs out of a terminal server and cleans up a directory in their home folder that was filling up with space due to application crashes.

This is the script I created:

$username = $ENV:USERPROFILENAME

$testpath = Test-Path -Path "$username\AppData\Local\Microsoft\Windows\ApplicationFolder"

if ($testpath -eq $true){

 $items = Get-ChildItem - Path "$username\AppData\Local\Microsoft\Windows\ApplicationFolder"

 foreach($i in $items){

  Remove-Item -Path "$username\AppData\Local\Microsoft\Windows\ApplicationFolder\$i" -Recurse -Confirm:$FALSE

 }

}else{}

This code will get the users profile root path and then check if the application folder exists, if it doesn’t then the script ends. If it does exist, the script will cycle through each entry and remove it.

The -Confirm:$FALSE parameter was added because the script kept asking for confirmation when deleting each item. This stops this behaviors and deletes each item without a confirmation prompt.

Now that I have the script and it is working as expected, I create a local group policy that will use:

Name – “powershell.exe”

Parameters – “-F “C:\path\to\file.ps1”

You can see this in the screenshot below:

Logoff script group policy

 

This group policy was added under:

User Configuration – Windows Settings – Scripts (Logon/Logoff) – Logoff

Hopefully you can replicate what I have done and don’t experience any issue. Note that you might need to change the script execution policy on the machine before this works properly. Just something to keep in mind if the group policy isn’t working. Enjoy!

Create, Store & Use Encrypted Passwords With PowerShell

This is just a neat little “tactic” I use when I need to connect to the same machine over and over again but don’t want to drive myself insane with having to constantly enter the same username and password. For example, when testing a script.

First you need to enter your password, in plain text, into this script so that it can get the password. This a perfectly safe as it will only be at this point where the password is in plain text.

$password = "PUT PASSWORD HERE" | ConvertTo-SecureString -AsPlainText -Force

This gets the password that you just entered and encrypts it and also puts it into the variable “password”

Now you need to convert the password to an encrypted string of characters using the below command:

$Password2 = $password | ConvertFrom-SecureString | Out-File "PATH TO TEXT FILE TO STORE PASSWORD"

This puts the encrypted password into the text file for later use.

Now, whenever you need to connect to a machine, you can put this into a variable along with the username. Then put them together into a credential and away you go:

$Username = "DOMAIN\username"

$EcryptedPassword = Get-Content "LOCATION TO TEXT FILE" | ConvertTo-SecureString

$Credential = New-Object System.Management.Automation.PSCredential($Username, $EncryptedPassword)

This builds the credential which you can now use with something similar to below:

Invoke-Command -Credential $Credential -ScriptBlock {echo "test"} -ComputerName "COMPNAME" -Authentication CredCSSP

Enjoy!