Jesse Harris IT

A blog about IT and other things I find interesting

Win32 OpenSSH Package

August 14, 2018 — Jesse Harris

I've recently been using Macos at work lately in order to share admin responsibilities across the team. Still suppporting Windows, however there are a couple of tools I use to make working Windows from a Mac simpler.

PowerShell

Microsoft Open-Sourced PowerShell in 2016 and today in 2018, you can get stable installations for Macos, Linux and Windows on github which is often referred to as PSCore.

As an aside, this new version of powershell is not nativly backward compatible with compiled binary modules of the previous "Windows Powershell", however recently in development is a new module: WindowsCompatibility (currently only available on Windows Insider builds) that allows your to import "Windows Powershell" modules into PSCore.

When alpha and beta builds first became available I started testing remote sessions from Linux and Macos to Windows (As I would prefer to work from a unix system at work), but quickly found that the native "Enter-PSSession" wasn't supported from PSCore.

OpenSSH

Around the same time, Microsoft began working with the OpenBSD's OpenSSH project to bring official OpenSSH builds to Windows and the PSCore team found a way to make "Enter-PSSession" work with this.

Packaging PSCore

Packaging PSCore is very straightforward and I won't go into detail here. Suffice it to say that PSCore is released as an MSI and these are very simple to deploy using tools like Configuration Manager.

OpenSSH Package

Essentially I created a Windows Powershell script which follows the installation directions on the Win32 OpenSSH github Installation page.

The Scripts

Deploying an application using scripts in Configuration Manager, usually requires 3 scripts, and this case is no exception. I have provided the all needed scripts below:

Install.ps1

[CmdLetBinding()]
Param()

#region Helper functions
function Get-Path {
    [CmdLetBinding()]
    Param(
        [ValidateSet(
            "Machine",
            "User"
        )]$Context = "User",
        [Switch]$Raw
    )
    If ($Context -eq "Machine") {
        $Root = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager'
    } else {
        $Root = 'HKCU:'
    }
    If ($Raw){
        Get-ItemPropertyValue -Path "$Root\Environment" -Name Path
    } Else {
        Try {

            (Get-ItemPropertyValue -Path "$Root\Environment" `
                -Name Path -EA SilentlyContinue) -split ';'
        } Catch {
            Write-Warning "No user environment variables found"
        }
    }
}

function Add-Path {
    [CmdLetBinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [ValidateScript({
            if (Test-Path -Path $_) {
                $True
            } else {
                throw "Unable to validate path $_"
            }
            })]$Path,
        [ValidateSet(
            "Machine",
            "User"
        )]$Context
    )

    Write-Verbose "Adding $Path to environment"
    if ($Context -eq 'Machine') {
        If (! $Path -in (Get-Path -Context Machine)){
            Write-Verbose "Adding $Path to machine context"
            setx /m PATH "$(Get-Path -Context Machine -Raw);$Path"
        }
    } else {
        Write-Verbose "Adding $Path to user context"
        If (! $Path -in (Get-Path -Context User)){
            Write-Verbose "Adding $Path to user context"
            setx PATH "$(Get-Path -Context Use -Raw);$Path"
        }
    }
}

function New-SymbolicLink {
Param($Link,$Target)
    If (-Not (Test-Path -Path $Link)){
        If ((Get-Item $Target).PSIsContainer) {
            cmd.exe /c mklink /D $Link $Target
        } Else {
            cmd.exe /c mklink $Link $Target
        }
    }
}
#endregion

# Extract OpenSSH
$Archive = Get-ChildItem -Filter *.zip
Expand-Archive -Path $Archive -DestinationPath $env:ProgramFiles
Rename-Item -Path $Env:ProgramFiles\OpenSSH-Win64 -NewName OpenSSH

#Add InstallDir to Path
Add-Path -Path $Env:ProgramFiles\OpenSSH -Context Machine -Verbose

# Configure OpenSSH
& $Env:ProgramFiles\OpenSSH\install-sshd.ps1

# Start sshd service

Start-Service -Name sshd

# Set service startup

Set-Service sshd -StartupType Automatic
Set-Service ssh-agent -StartupType Automatic

# Setup pwsh link to work around 
# https://github.com/PowerShell/Win32-OpenSSH/issues/784
# Find PSCore Install and Make symbolic link

$PSCoreDir = Get-ChildItem -Path $env:ProgramFiles\PowerShell `
    -Directory | Select-Object -Last 1
New-SymbolicLink -Link $env:SystemDrive\pwsh -Target $PSCoreDir.FullName

# Enable Password Authentication and set pwsh as default shell
$NewConfig = Get-Content -Path $Env:ProgramData\ssh\sshd_config | 
    ForEach-Object {
    Switch ($_) {
        {$_ -match '^#PasswordAuthentication\syes'} {$_.replace('#','')}
        {$_ -match '^#PubkeyAuthentication\syes'} {$_.replace('#','')}
        {$_ -match '^Subsystem\s+sftp\s+'} {
            'Subsystem    powershell c:\pwsh\pwsh.exe -sshs -NoLogo -NoProfile'
        }
        Default {$_}
    }
}
# Update sshd config
Set-Content -Path $Env:ProgramData\ssh\sshd_config -Value $NewConfig `
    -Force

# Restart sshd
Restart-Service sshd

Uninstall.ps1

[CmdLetBinding()]
Param()

#region Helper functions

    function Remove-SymbolicLink {
Param($Link,$Target)
    If (Test-Path -Path $Link){
        If ((Get-Item $Target).PSIsContainer) {
            cmd.exe /c rmdir $Link

        } Else {
            cmd.exe /c del $Link

        }

    }

    }

function Get-Path {
    [CmdLetBinding()]
        Param(
                [ValidateSet(
            "Machine",
            "User"

                    )]$Context = "User",
        [Switch]$Raw

             )
            If ($Context -eq "Machine") {
        $Root = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager'

            } else {
        $Root = 'HKCU:'

            }
    If ($Raw){
        Get-ItemPropertyValue -Path "$Root\Environment" -Name Path

    } Else {
        Try {

            (Get-ItemPropertyValue -Path "$Root\Environment" `
                -Name Path -EA SilentlyContinue) -split ';'

        } Catch {
            Write-Warning "No user environment variables found"

        }

    }

}

function Remove-Path {
    [CmdLetBinding()]
        Param(
        [Parameter(Mandatory=$True)]
        $Path,
        [ValidateSet(
            "Machine",
            "User"

            )]$Context

             )

    Write-Verbose "Removing $Path from environment"
    if ($Context -eq 'Machine') {
        If ($Path -in (Get-Path -Context Machine)){
            Write-Verbose "Removing $Path from machine context"
            $NewPath = ""
            Get-Path -Context Machine | Where-Object {
                $psItem -ne $Path -and
                $psItem -ne ""

            } ForEach-Object {
                    $NewPath += "$psItem;"

            }
            setx /m PATH "$NewPath"

        }

    } else {
        Write-Verbose "Removing $Path from user context"
            If ($Path -in (Get-Path -Context User)){
            Write-Verbose "Removing $Path from user context"
            $NewPath = ""
            Get-Path -Context User | Where-Object {
                $psItem -ne $Path -and
                $psItem -ne ""

            } ForEach-Object {
                    $NewPath += "$psItem;"

            }
            setx PATH "$NewPath"

            }

    }

}

#endregion

& $Env:ProgramFiles\OpenSSH\uninstall-sshd.ps1

# Extract OpenSSH
Remove-Item -Path $env:ProgramFiles\OpenSSH -Recurse -Force
Remove-Path -Path $env:ProgramFiles\OpenSSH -Context Machine -Verbose

# Find PSCore Install and remove symbolic link

$PSCoreDir = Get-ChildItem -Path $env:ProgramFiles\PowerShell -Directory | Select-Object -Last 1
Remove-SymbolicLink -Link $env:SystemDrive\pwsh -Target $PSCoreDir.FullName

# Remove old config
Remove-Item -Path $env:ProgramData\ssh -Recurse -Force

Detect.ps1

$AssumeInstalled = $True

If (-Not (Test-Path $Env:ProgramFiles\OpenSSH)) {
    $AssumeInstalled = $False

}

If (-Not (Test-Path $Env:SystemDrive\pwsh)) {
    $AssumeInstalled = $False

}

If (-Not (Get-Service sshd -ErrorAction SilentlyContinue)) {
    $AssumeInstalled = $False

}

If ($AssumeInstalled) {
    Write-Output "True"

}

Using OpenSSH with Powershell

Now that I have used these scripts to deploy OpenSSH and PSCore, I can PSRemote to a PC using my Mac.

The old way to use "Enter-PSSession" was by specifying the ComputerName parameter like so:

PS\> Enter-PSSession -ComputerName Blah

However, when using OpenSSH with a PS Session you do the following:

PS\> Enter-PSSession -HostName Blah -UserName MrBlah

You could also setup a session in a variable and resue it multiple times in a session:

PS\> $s = New-PSSesssion -HostName Blah -UserName MrBlah
PS\> Enter-PSSession -Session $s

[Blah] PS\>

I hope you have found this usefull.

Tags: powershell, pscore, openssh, windows, macos

Comments? Tweet  

New-AppVSequencerVM

April 08, 2017 — Jesse Harris

In Windows 10 1703 microsoft introduced some new Powershell Commandlets for APPV. New-AppVSequencerVM and New-BatchAppVSequencerPackages and Connect-AppVSequencerVM

In this post, I will explorer using these commandlets to package some software.

First we have to download some tools. In this blog post, I've used Convert-WindowsImage from the Technet Gallery to convert a Windows 10 ISO to a VHDX. You can get it from here: Download Convert-WindowsImage.ps1: TechNet Gallery

Download an Enterprise Creators Update ISO (I got mine from my MSDN account)

Download Windows ADK 1703: ADK Download

First here is the commands I ran to convert my 1703 ISO to a vhdx:

The 'Convert-WindowsImage.ps1' script only contains functions like a module, so dot source it to load the functions:

C:\>cd SeqTest
C:\SeqTest\> . .\Convert-WindowsImage.ps1
C:\SeqTest\> Convert-WindowsImage `
-SourcePath Win10-1703.iso `
-Edition Enterprise -WorkingDirectory .\

Tags: appv, powershell, 1703, adk

Comments? Tweet  

Yet another Dell Warranty Configuration Manager Hardware Inventory Extension Post

August 24, 2016 — Jesse Harris

You've read it before, post upon post upon post. You've trawled through the comments searching for answers, analysing html data, scraping websites, fudging data and whatnot. All for what?

The Holy Grail of Inventory extensions

Configuration Manager has its roots in client management, not Asset management. Microsoft have added Asset Intelligence capabilities to Configuration Manager and improved its usefulness as an Asset Management tool, but it still has a long way to go. For example, other tools specifically designed for Asset Management would be able to perform this task by simply importing a spreadsheet.

A quick overview of the steps for this:

  1. Gather the data

    Scrape a website or batch process with Lenovo

  2. Make the data available

    Make it so machines can query a service to find their warranty info from your network without them having to scrape a site for you

  3. Deploy a script for workstations to ingest data into their registry

    Each client device will record its warranty end date in registry

  4. Extend the Inventory

    Configuration Manager will run a mofcomp of the site configurationmanager.mof file. This copies the registry data into WMI

  5. Run a hardware inventory

    This will gather the data and import it into the Configuration Manager SQL database

  6. Inject the database with remaining data to speed up the process.

    Many devices won't run the script or connect to our system quick enough for us to have useful data. I'm going to do the unthinkable and SQL INSERT my data into the SQL DB

  7. Create a Collection based on devices due to be replaced in Q4 2016

Gather the data

Dell

Every man and his dog has written a web scraper for Dells site. I did this years ago when a higher up wanted to fill in the blanks. People like me are why Dell now put a capcha block in front of the warranty info. It's annoying but it's not a show-stoppa.

Shipping Date

Dell still shows the shipping date plain and simple on their site, so it's not too hard to go from Shipping Date + Warranty period to arrive at Warranty End Date.

For us this is easy, we buy all our Desktops with 4 years warranty and all our laptops with 3 years warranty.

Here is the code:

function Get-DellWarrantyStatus {
    <#  
        .SYNOPSIS
        Checks to see the input computername matches what a Dell/Lenovo 
        service tag should be. If so, checks a looks up cache csv file for a 
        matching record and returns warranty end date. If the record is 
        not found in the CSV, the machine Manufacturer is retrieved from 
        ConfigMgr and then the data is retrieved from the Dell or Lenovo 
        website displayed and stored locally.

        .DESCRIPTION
        Usefull tool for looking up Warranty end date of Dell and Lenovo hardware.

        .PARAMETER ComputerName
        Specifies a ComputerName record to query. Must correspond with a 
        dell servicetag or Lenovo Serial number.

        .EXAMPLE
        C:\PS>Get-DellWarrantyStatus -ComputerName 6wmpsn1

        ComputerName WarrantyEndDate        Source
        ------------ ---------------        ------
        6wmpsn1      19/11/2013 12:00:00 AM Cache       

        .EXAMPLE
        C:\PS>Get-DellWarrantyStatus (Get-Content D:\temp\computers.txt)

        ComputerName WarrantyEndDate        Source
        ------------ ---------------        ------
        2WDX92S      31/08/2015 12:00:00 AM Remote
        JTDX92S      31/08/2015 12:00:00 AM Remote
        5WDX92S      31/08/2015 12:00:00 AM Remote
        7LCX92S      31/08/2015 12:00:00 AM Remote

        .NOTES
        Author: Jesse Harris
        For: University of Sunshine Coast
        Date Created: 26 October 2012        
        ChangeLog:
        1.0 - First Release
        2.0 - Added Lenovo as a supported Manufacturer
    #>
    [CmdletBinding()]
    Param(
        [Parameter(
            ValueFromPipeline=$True,
            ValueFromPipelinebyPropertyName=$True)]
                [string[]]$ComputerName="$env:computername"
    )

    Begin {
        $CacheFile = $env:AppData+'\USC\DellWarranty.csv'
        If (Test-Path $CacheFile) {
            $Cache = Import-CSV -Path $CacheFile
        } Else {
            New-Item -ItemType File -Path $CacheFile -Force
        }
    }

    PROCESS {

        function Get-DWWorker {
            Param($Name)
            If (($Cache) -and ($Name -in $Cache.ComputerName)) {
                $Cache | Where-Object {$_.ComputerName -eq $Name}
            } Else {
                #Dell Code
                $Model = (Get-CfgClientInventory -ComputerName $Name -Properties Model).Model
                Write-Verbose "Model $Model"
                $WarrantyURL = 
                "http://www.dell.com/support/home/us/en/19/product-support/servicetag/" + `
                    "$Name/configuration"
                try {
                    $WarrantyPage = Invoke-WebRequest -Uri $WarrantyURL
                } catch [System.SystemException] {
                    Write-Error "Unable to communicate with Warranty URL"
                    return
                }
                $WPageText = $WarrantyPage.AllElements | ?{$_.id -eq "subSectionA"} `
                    | select -ExpandProperty outertext
                [regex]$regex = '\d{1,2}/\d{1,2}/\d{4}'
                $DateString = ([string]($regex.Matches($WPageText)))
                Write-Verbose "Datestring = $DateString"
                $DateStringSplit = $DateString -Split "/"
                #Write-Verbose $DateString -Verbose
                try {
                    $Day = [Decimal]$DateStringSplit[0].ToString("00")
                    $Month = [Decimal]$DateStringSplit[1].ToString("00")
                    $Yeat = [Decimal]$DateStringSplit[2]
                    $ConvertedDate = "$Day/$Month/$Year"
                    $DateObject = 
                        [DateTime]::ParseExact($ConvertedDate,"d",[cultureinfo]::InvariantCulture)
                } catch [System.SystemException] {
                    Write-Error "Cannot convert DateTime, Page structure may have changes. Contact Jesse"
                    BREAK
                }
                Write-Verbose "Adjusting Date for Model ""$Model"""
                Switch ([string]$Model) {
                    {$_ -match "Latitude"} {
                        #Add 3 years to ship date
                        Write-Verbose "Adding 3 years to ship date"
                        $Date = (Get-Date $DateObject).AddYears(3)
                    }
                    {$_ -match "OptiPlex"} {
                        #Add 4 years to ship date
                        Write-Verbose "Adding 4 years to ship date"
                        $Date = (Get-Date $DateObject).AddYears(4)
                    }
                    {$_ -match "Precision M"} {
                        #Add 3 years
                        Write-Verbose "Adding 3 years to ship date"
                        $Date = (Get-Date $DateObject).AddYears(3)
                    }
                    default {
                        #Add 4 years default
                        Write-Verbose "Unable to detect model, adding 4 years to ship date"
                        $Date = (Get-Date $DateObject).AddYears(4)
                    }
                }
            }
            $WarrantyEOL = New-Object -TypeName PSCustomObject
            $WarrantyEOL | Add-Member -MemberType NoteProperty -Name ComputerName -Value $Name
            $WarrantyEOL | Add-Member -MemberType NoteProperty -Name WarrantyEndDate -Value (Get-Date $Date)
            #Update cache in memory
            $Cache = $Cache + $WarrantyEOL
            #Update cache on disk
            $WarrantyEOL | Export-CSV -Append -Path $CacheFile -Force
            #Show result
            $WarrantyEOL | Add-Member -MemberType NoteProperty -Name Source -Value "Remote" -PassThru
        }
    }

    If ($PSBoundParameters.ContainsKey('ComputerName')) {
        Foreach ($Computer in $ComputerName) {
            Get-DWWorker -Name $Computer
        }
    } Else {
        Get-DWWorker -Name $ComputerName
    }
}

Tags: powershell, dell

Comments? Tweet