Validating JSON Web Token (JWT) with PowerShell

By popular demand, finally my second blog post online. Please feel free to feedback to me what you think about it.

Validating your JSON Web Token (JWT) with PowerShell
In some organizations, where you have a segregation in the management infrastructure, you might only manage part of the infrastructure (i.e. WebServers) and not the ADFS part of the infrastructure. In this case you are only the Administrator on the WebServer, you are just a normal user of the ADFS infrastructure. Your ADFS Administrator has setup ADFS for you, in line with the requirements that came with your application.

Background WAP and JSON Web Token (JWT):
In this example we will use Windows Azure Pack (WAP) as the application running on the WebServer. Several blogs have been written on how to setup ADFSv3 with WAP so it generates the required JSON Web Token (JWT). Referral to some by Marc van Eijk listed below:
http://www.hyper-v.nu/archives/mvaneijk/2014/01/windows-azure-pack-with-adfs-and-windows-azure-multi-factor-authentication-part-1/
http://www.hyper-v.nu/archives/mvaneijk/2014/02/windows-azure-pack-with-adfs-and-windows-azure-multi-factor-authentication-part-2/
http://www.hyper-v.nu/archives/mvaneijk/2014/03/windows-azure-pack-with-adfs-and-windows-azure-multi-factor-authentication-part-3/

Background WAP Tenant Public API:
A useful tool (by Ben Gelens) which we combine with the above setup of WAP & JSON Web Token (JWT) is the PowerShell Module WAPTenantPublicAPI. Just fire up your PowerShell 5 and install via the PowerShell Gallery:

Install-Module -Name WAPTenantPublicAPI

 

The use case – Validating your JSON Web Token (JWT) with PowerShell
Let’s go! Get a JWT Token:

JWT_Pic1_Desktop

Get-WAPToken -Credential (Get-Credential -UserName menno.stevens@madeofthings.net -Message 'ADFS Account') –URL https://fs.madeofthings.net -ADFS

Get-WAPToken is a CmdLet in the WAPTenantPublicAPI. The CmdLet formats the request for your ADFS infrastructure in the right way, authenticates and stores your token in the $token variable.JWT_Pic2_Desktop

 

Decoding your JSON Web Token (JWT) with PowerShell
Let’s decode!

As per JWT Specifications (https://tools.ietf.org/html/rfc7519), the token is split in 3 parts, separated by a ‘.’ (dot). The easiest way I know to decode the first 2 parts with PowerShell is using these few lines (core from Shriram [MSFT] in the Technet Gallery):

foreach ($i in 0..1) {
    $data=$token.Split('.')[$i].Replace('-', '+').Replace('_', '/')
    switch ($data.Length % 4) {
        0 {break}
        2 {$data += '=='}
        3 {$data += '='}
    }
    [System.Text.Encoding]::UTF8.GetString([convert]::FromBase64String($data))
    '---'
}

The result is shown below:JWT_Pic3b_DesktopBut what about the 3rd part?
That is the signature proving the token is not tampered with. And we can read it easily.
Using the same few PowerShell script-lines, just replace “foreach ($i in 0..1)” with “foreach ($i in 0..2)”:JWT_Pic4_DesktopRight… But that 3rd part of your token (the garbage) does not proof anything. We can’t validate the JWT Token that way.

 

Validating your JSON Web Token (JWT) with PowerShell
Let’s validate!

As explained in the introduction, we use Windows Azure Pack (WAP) as the example application. You have to retrieve the ADFS/JWT Certificate you are going to validate your Token against from your configuration. Or get the Certificate from your ADFS Administrator.
To be short, in our example case, login to the Azure Pack Webserver and retrieve the installed certificate using the following commands. And store it in the $cert variable which MUST be of the type “X509Certificate2”.

$Connectionstring='Data Source=MYSQLSERVER;Integrated Security=SSPI;Trusted_Connection=yes'
$TenantSiteTenant=Get-MgmtSvcRelyingPartySettings -ConnectionString $Connectionstring -Target 'Tenant'
$certs=$TenantSiteTenant.Certificates|%{[System.Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($_))}
$cert=$certs|where Issuer -like 'CN=DigiCert*'

JWT_Pic5_Desktop

PowerShell and Windows don’t really understand the JSON Web Token (JWT). But again, PowerShell 5 to the rescue. Get the Package “System.IdentityModel.Tokens.Jwt” from the NuGet Repository. I found that I got the best results with version 3.0.2. You might want to improve my scripting and try a newer version. Get it installed with this command:

Install-Package System.IdentityModel.Tokens.Jwt -Version 3.0.2

And load the DLL into PowerShell with:

Add-Type -Path 'C:\the..absolute..path..to\v3.02\net45\System.IdentityModel.Tokens.Jwt.dll'

Now the script-lines that actually will validate your JWT Token in the $token variable against the Certificate in the $cert variable:

$X509SecurityToken=New-Object System.IdentityModel.Tokens.X509SecurityToken($cert)
$JwtSecurityToken=(New-Object System.IdentityModel.Tokens.JwtSecurityTokenHandler).ReadToken($token)
$TokenProperties=@{
    SigningToken=$X509SecurityToken;
    AllowedAudience=$JwtSecurityToken.Audience;
    ValidIssuer=$JwtSecurityToken.Issuer
}
$TokenValidationParameters=New-Object System.IdentityModel.Tokens.TokenValidationParameters -Property $TokenProperties
$ValidateTokenResult=(New-Object System.IdentityModel.Tokens.JwtSecurityTokenHandler).ValidateToken($JwtSecurityToken,$TokenValidationParameters)
Write-Verbose -Verbose "The token validation result is: $($ValidateTokenResult.Identity.IsAuthenticated)"

Let’s see some results.

The below is an example where a valid $token is validating against the wrong $cert: JWT_Pic6a_Desktop
Here an example where a valid $token is validating against a valid $cert, but the CRL Service of the CA was not available: JWT_Pic6b_Desktop
Now a valid $token, against a valid $cert with the CRL Service of the CA running. But with an obvious explanation “The token is expired”: JWT_Pic6c_Desktop

Now some messing around with the $token. And result is that something totally has gone wrong “Check to make sure the SecurityAlgorithm is supported”: JWT_Pic6d_Desktop
And finally, this one seems right:
JWT_Pic6e_Desktop
So with this Boolean value of $ValidateTokenResult.Identity.IsAuthenticated being “True”, we can say we have actually validated that the value of $token is a valid one. Now it is save to use the claims provided.

Advertisements

Automated version control of SMA Runbooks to GitHub (or Git, or GitLab)

As this is the first post on my blog, please feel free to read something about myself on the About page.

The challenge

While working with Windows Azure Pack (WAPack) and Service Management Automation (SMA) for the last 18 months, the biggest drawback for me: Version Control of Runbooks that are stored in SMA. In short: It’s not there. Yes, we have the “Draft” and “Publish” status, but that is really limited. Yes, we can export with PowerShell and store the Runbooks offline (i.e. Using SMART), it does a great job, but limited as it does not track the actual changes line-by-line inside your Runbook.

Beside TFS, there are some other great (free or paid) version control repositories available. Git being one of them. Either hosted on GitHub.com, build yourself on premise/build yourself in the Azure Cloud or get GitLab from the VM Depot (ready to run on Azure). Assumption is that you have one of these in place for your organization. When you have TFS already running as your version control repository, look here.

So how does Git work? That is probably too detailed to explain here, and me being not the best person to ask. Assuming your have your repository running in a few clicks, I found the following documentation a valuable resource to start reading: http://git-scm.com/doc

At least you need a Git Client. If you have PowerShell 5.0 (WMF5) you can use OneGet to get it from Chocolatey.org, or from the internal NuGet Repository in your organization. Or use the old fashioned download, get the Git Client from here: http://git-scm.com/download/win

Play around, explore, start with the following commands on your Windows command line: “git clone <url>”, “git init”, “git add .”, “git commit -m done”, “git push” and “git pull”.

And those are the commands we want to Automate in a SMA Runbook, to export all our valuable SMA Runbooks to a proper version control repository and keep track of all changes line-by-line. What I have done until now is one-way, assuming SMA is your “golden source” always pushing changes to Git.

Prerequisites

I assume that certain prerequisites are already in place:

  • SMA Runbook workers on level UR4 with Windows Server 2012R2 and PowerShell 4.0
  • The binaries of the Git Client copied to “C:\Git” on all your SMA Runbook workers. With “C:\Git\bin\git.exe” being the path to the executable, no need to install, just copy the installed “Git” directory to the SMA Runbook workers from a valid source (like your workstation, running PowerShell 5.0 that was getting it from Chocolately.org).
  • A Git repository filled with at least 1 file (best practice: Place a file named “README.md” with the first line of text telling something about this repository)
  • The URL for your Git repository, like “git@fqdnname.domain.com:USERNAME/REPOSITORY.git”
  • The Public/Private keypair files (“.\id_rsa” and “.\id_rsa.pub”) without password, that is already working with your Git repository of choice via SSH. We will explain how to secure those keys with SMA.
  • The “.\known_hosts” file which references to your Git repository being trustworthy.
  • The firewall should be open from you SMA Runbook workers towards the Git repository.

 Preparing SMA

We also assume certain other things are in place before we can run our Runbook:

 Creating a SMA Variable for the Key-pair files

One of the jobs in preparing SMA is to create a variable of the type Complex (Encrypted) with  content the following 3 files: Public Key, Private Key, Known Hosts. The script below does create this SMA Variable.

$WebServiceEndpoint=&quot;https://sma.domain.com&quot;
$complexvarname=&quot;GitKey-Export-SMARunbooks&quot;

$pvtfilename=&quot;.\id_rsa&quot;
$pubfilename=&quot;.\id_rsa.pub&quot;
$knownhostsfilename=&quot;.\known_hosts&quot;

[string]$pvtmultiline=$null
[string]$pubmultiline=$null
[string]$knownhostsmultiline=$null
Get-Content $pvtfilename|%{$pvtmultiline+=($_+&quot;`n&quot;)}
Get-Content $pubfilename|%{$pubmultiline+=($_+&quot;`n&quot;)}
Get-Content $knownhostsfilename|%{$knownhostsmultiline+=($_+&quot;`n&quot;)}

$keyscomplextype=@{
  &quot;PrivateKey&quot;=($pvtmultiline|ConvertTo-Json)
  &quot;PublicKey&quot;=($pubmultiline|ConvertTo-Json)
  &quot;KnownHosts&quot;=($knownhostsmultiline|ConvertTo-Json)
}

# Remove -Encrypted switch when Testing outside SMA
Set-SmaVariable -Name $complexvarname -Value $keyscomplextype -WebServiceEndpoint $WebServiceEndpoint -Encrypt

 

The actual Runbook

Note: The entire script can be found at the end of this post

I will not be discussing the Runbook in detail here. Basically is comes down to one big inlinescript after some rudimentary Variables as preperation.

Inside the inlinescript, you can distinguish the following sections:

  • Preparing Windows (User) Environment variables
  • Exporting the Keypair files (imported above)
  • Starting the first git.exe commands to setup the Git environment: Username, Email and Default push mode
  • Now switch to using sh.exe instead of git.exe (git is still used as part of the -c switch)

Reason: sh.exe automatically reads and uses the Keypair files correctly without having to setup additional command-line switches or Environment variables.

  • Pulling your initial Clone from the Git repository with “–depth 1” as only need to control and push back the latest version and not years of history
  • Removing all Runbooks from the $gitrunbooksubdir (“.\Runbooks”) directory  locally, that were just cloned/copied there using the “git clone <uri> –depth 1” command
  • Newly Export all Published SMA Runbooks to the same, just cleared directory $gitrunbooksubdir
  • Letting Git itself sort out the differences between the Removed files and newly Exported files:
    •  Using the commands “git add . –all”, “git commit -m <timestamp>” and “git push”
  • Finally, cleaning up all temporary Files, Directories, Keys and Environment variables

The result

As an end result you will see the Runbook in your Git repository. Some quick screenshot below:

sc_git1

An example change of a Runbook editted in SMA. Showing below how you will find this back in Git:

sc_git2

Another tip

Schedule this Runbook, once a day, twice a day, hourly and never worry about losing track of changes in your Runbooks again.

sc_git3

The entire script (for download) to be used as SMA Runbook:

Note: Click on Expand, to Expand this section and see the code.

workflow Export-RunbooksToGit
{
    param (
        [string]$GitKey=&quot;GitKey-Export-SMARunbooks&quot;,
        [string]$GitSSHProjectURL=&quot;git@github.com:USERNAME/export-smarunbooks.git&quot;,
        [string]$GitProjectName=&quot;export-smarunbooks&quot;,
        [string]$GitRunbookSubdirName=&quot;Runbooks&quot;,
        [string]$GitWin32Path=&quot;C:\Git\bin&quot;,
        [string]$SmaConnectionName=&quot;SmaConnection&quot;
    )
        
    # === BEGIN-SECTION FOR PREPARING VARIABLE RELATED TO SMARUNBOOK ENVIRONMENT ===
    $SMAConnection=Get-AutomationConnection -Name $SmaConnectionName

    $SMAUserName=inlinescript{($Using:SMAConnection).Get_Item(&quot;UserName&quot;)}
    $SMAUserPassword=inlinescript{($Using:SMAConnection).Get_Item(&quot;UserPassword&quot;)}
    $SMASecureUserPassword=ConvertTo-SecureString -AsPlainText -String $SMAUserPassword -Force
    $SMAWebServicePort=inlinescript{($Using:SMAConnection).Get_Item(&quot;WebServicePort&quot;)}
    $SMAWebServiceHostnameWithProtocol=inlinescript{($Using:SMAConnection).Get_Item(&quot;WebServiceHostnameWithProtocol&quot;)}
    $SMACredential=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $SMAUserName,$SMASecureUserPassword        
    # === END-SECTION FOR PREPARING VARIABLE RELATED TO SMARUNBOOK ENVIRONMENT ===

    # Reading Public/Private keys from SMA Variable Assets
    $GitKeyComplex=Get-AutomationVariable -Name $GitKey    

    inlinescript {
        $VerbosePreference=[System.Management.Automation.ActionPreference]$Using:VerbosePreference
        $DebugPreference=[System.Management.Automation.ActionPreference]$Using:DebugPreference

        $workdir=[System.IO.Path]::GetTempPath()+[System.IO.Path]::GetRandomFileName()        

        $gitprojectdir=$workdir+&quot;\&quot;+$Using:GitProjectName
        $gittestdir=$gitprojectdir+&quot;\.git\&quot;
        $gitrunbooksubdir=$gitprojectdir+&quot;\&quot;+$Using:GitRunbookSubdirName

        $pvtsinglelinefromsma=$Using:GitDeployKeyComplex.PrivateKey|ConvertFrom-Json
        $pubsinglelinefromsma=$Using:GitDeployKeyComplex.PublicKey|ConvertFrom-Json
        $knownhostssinglelinefromsma=$Using:GitDeployKeyComplex.KnownHosts|ConvertFrom-Json
        $sshdir=$env:USERPROFILE+&quot;\.ssh&quot;
        $pvtfilename=$sshdir+&quot;\id_rsa&quot;
        $pubfilename=$sshdir+&quot;\id_rsa.pub&quot;
        $knownhostsfilename=$sshdir+&quot;\known_hosts&quot;

        # Create WORK directory
        if (-not (Test-Path $workdir)) {$tmpnewitem=New-Item -Force -ItemType Directory -Path $workdir}

        # Write SSH Security Keys required for Git connection
        if (-not (Test-Path $sshdir)) {$tmpnewitem=New-Item -Force -ItemType Directory -Path $sshdir}
        [IO.File]::WriteAllText($pvtfilename,$pvtsinglelinefromsma)
        [IO.File]::WriteAllText($pubfilename,$pubsinglelinefromsma)
        [IO.File]::WriteAllText($knownhostsfilename,$knownhostssinglelinefromsma)

        # $Using:GitWin32Bin add to environment 'path' (appending to 'current user')
        $ExistingUserpath=[System.Environment]::GetEnvironmentVariable(&quot;PATH&quot;,&quot;User&quot;)
        $UserpathElements=@([System.Environment]::GetEnvironmentVariable(&quot;PATH&quot;,&quot;User&quot;) -split &quot;;&quot;)
        $UserpathElements+=$Using:GitWin32Path
        $NewUserPath=$UserpathElements -join &quot;;&quot;
        [System.Environment]::SetEnvironmentVariable(&quot;PATH&quot;,$NewUserPath,&quot;User&quot;)

        # USERPROFILE and HOME environment varables are not available inside Workflow/inlinescript/Start-Process-combo
        [System.Environment]::SetEnvironmentVariable(&quot;HOME&quot;,$env:USERPROFILE,&quot;User&quot;)
        [System.Environment]::SetEnvironmentVariable(&quot;USERPROFILE&quot;,$env:USERPROFILE,&quot;User&quot;)

        #Start-Process -FilePath $env:ComSpec -Wait -UseNewEnvironment
        $OutputFile=[System.IO.Path]::GetTempFileName()
        $ErrorFile=[System.IO.Path]::GetTempFileName()

        &quot;=-=-=configname&quot;|Write-Debug
        $confignameargument=&quot;config --global user.name `&quot;SMARBW &quot;+$env:COMPUTERNAME+&quot;`&quot;&quot;
        Start-Process -FilePath ($Using:GitWin32Path+&quot;\git.exe&quot;) -Wait -UseNewEnvironment -LoadUserProfile -RedirectStandardOutput $OutputFile  -RedirectStandardError $ErrorFile `
                -ArgumentList $confignameargument -WorkingDirectory $workdir
        Get-Content $OutputFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;Welcome to Git*&quot; -and $_ -notlike &quot;*git help*&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $OutputFile) {Remove-Item -Force -Path $OutputFile}
        Get-Content $ErrorFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $ErrorFile) {Remove-Item -Force -Path $ErrorFile}

        &quot;=-=-=configemail&quot;|Write-Debug
        $configemailargument=&quot;config --global user.email `&quot;&quot;+$env:COMPUTERNAME+&quot;@domain.com`&quot;&quot;
        Start-Process -FilePath ($Using:GitWin32Path+&quot;\git.exe&quot;) -Wait -UseNewEnvironment -LoadUserProfile -RedirectStandardOutput $OutputFile  -RedirectStandardError $ErrorFile `
                -ArgumentList $configemailargument -WorkingDirectory $workdir
        Get-Content $OutputFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;Welcome to Git*&quot; -and $_ -notlike &quot;*git help*&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $OutputFile) {Remove-Item -Force -Path $OutputFile}
        Get-Content $ErrorFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $ErrorFile) {Remove-Item -Force -Path $ErrorFile}

        &quot;=-=-=configpushsimple&quot;|Write-Debug
        Start-Process -FilePath ($Using:GitWin32Path+&quot;\git.exe&quot;) -Wait -UseNewEnvironment -LoadUserProfile -RedirectStandardOutput $OutputFile  -RedirectStandardError $ErrorFile `
                -ArgumentList &quot;config --global push.default simple&quot; -WorkingDirectory $workdir
        Get-Content $OutputFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;Welcome to Git*&quot; -and $_ -notlike &quot;*git help*&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $OutputFile) {Remove-Item -Force -Path $OutputFile}
        Get-Content $ErrorFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $ErrorFile) {Remove-Item -Force -Path $ErrorFile}

        &quot;=-=-=clone&quot;|Write-Debug
        Start-Process -FilePath ($Using:GitWin32Path+&quot;\sh.exe&quot;) -Wait `
                -RedirectStandardOutput $OutputFile  -RedirectStandardError $ErrorFile `
                -ArgumentList (&quot;--login -c '/bin/git clone &quot;+$Using:GitSSHProjectURL+&quot; --depth 1'&quot;) -WorkingDirectory $workdir
        Get-Content $OutputFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;Welcome to Git*&quot; -and $_ -notlike &quot;*git help*&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $OutputFile) {Remove-Item -Force -Path $OutputFile}
        Get-Content $ErrorFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $ErrorFile) {Remove-Item -Force -Path $ErrorFile}

        # TEST SECTION - If the &quot;git clone&quot; command failed, the directory does not exist
        if (-not (Test-Path $gittestdir)) {
            &quot;Error - This directory should exist after the git.exe-clone command, directory does not exist: &quot;+$gittestdir|Write-Output

            # Cleanup (identical to Cleanup in error/exit section below)
            if (Test-Path $workdir) {Remove-Item -Force -Recurse -Path $workdir}
            if (Test-Path $sshdir) {Remove-Item -Force -Recurse -Path $sshdir}
            if (Test-Path ($Env:USERPROFILE+&quot;\.gitconfig&quot;)) {
                Remove-Item -Force -Recurse -Path ($Env:USERPROFILE+&quot;\.gitconfig&quot;)}
            if (Test-Path ($Env:USERPROFILE+&quot;\.bash_history&quot;))  {
                Remove-Item -Force -Recurse -Path ($Env:USERPROFILE+&quot;\.bash_history&quot;)}
            if (Test-Path $OutputFile) {Remove-Item -Force -Path $OutputFile}
            if (Test-Path $ErrorFile) {Remove-Item -Force -Path $ErrorFile}
            [System.Environment]::SetEnvironmentVariable(&quot;PATH&quot;,$ExistingUserpath,&quot;User&quot;)
            [System.Environment]::SetEnvironmentVariable(&quot;HOME&quot;,$null,&quot;User&quot;)
            [System.Environment]::SetEnvironmentVariable(&quot;USERPROFILE&quot;,$null,&quot;User&quot;)

            &quot;Error - This directory should exist after the git.exe-clone command, directory does not exist: &quot;+$gittestdir|Write-Error
        }

        # Although cloned, we need a Subdirectory for exporting the Runbooks to
        # But first, we want changes, also deletions of Runbooks, so let's remove everything :)
        if (Test-Path $gitrunbooksubdir) {Remove-Item -Force -Recurse -Path $gitrunbooksubdir}
        if (-not (Test-Path $gitrunbooksubdir)) {$tmpnewitem=New-Item -Force -ItemType Directory -Path $gitrunbooksubdir}

        # Exporting the Published Runbooks
        $runbooks=Get-SmaRunbook -WebServiceEndpoint $Using:SMAWebServiceHostnameWithProtocol -Port $Using:SMAWebServicePort -Credential $Using:SMACredential
        foreach ($runbook in $runbooks) {
                 Write-Debug &quot;RunbookName/ID:&quot;
                 $runbook.RunbookName|Write-Debug
                 $runbook.RunbookID|Write-Debug
                 $runbookdefinition=Get-SmaRunbookDefinition `
                    -Id $runbook.RunbookID `
                    -Type Published `
                    -WebServiceEndpoint $Using:SMAWebServiceHostnameWithProtocol `
                    -Port $Using:SMAWebServicePort `
                    -AuthenticationType &quot;Windows&quot; `
                    -Credential $Using:SMACredential
                 $runbookdefinition.Content|Set-Content -Path ($gitrunbooksubdir+&quot;\&quot;+$runbook.RunbookName+&quot;.ps1&quot;) -Force
        }

        # === BEGIN RUNBOOK OUTPUT ===
        # Pre-output for RunBook of what will be processed by the git.exe commands        
        $runbooks|select RunbookName,LastModifiedTime,LastModifiedBy|Sort-Object -Property RunbookName|ft -a|Out-File -FilePath $OutputFile -Force
        Get-Content $OutputFile
        &quot;---&quot;|Write-Output
        if (Test-Path $gitprojectdir) {
            Get-ChildItem -File $gitprojectdir|select Length,LastWriteTime,FullName|Sort-Object -Property FullName|ft -a|Out-File -FilePath $OutputFile -Force
            Get-Content $OutputFile
            &quot;---&quot;|Write-Output
        }

        if (Test-Path $gitrunbooksubdir) {
            Get-ChildItem -File $gitrunbooksubdir|select Length,LastWriteTime,Name|Sort-Object -Property Name|ft -a|Out-File -FilePath $OutputFile -Force
            Get-Content $OutputFile
            &quot;---&quot;|Write-Output
        }
        # === END RUNBOOK OUTPUT ===

        &quot;=-=-=adddot&quot;|Write-Debug
        Start-Process -FilePath ($Using:GitWin32Path+&quot;\sh.exe&quot;) -Wait `
                -RedirectStandardOutput $OutputFile  -RedirectStandardError $ErrorFile `
                -ArgumentList (&quot;--login -c '/bin/git add --all .'&quot;) -WorkingDirectory $gitprojectdir
        Get-Content $OutputFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;Welcome to Git*&quot; -and $_ -notlike &quot;*git help*&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $OutputFile) {Remove-Item -Force -Path $OutputFile}
        Get-Content $ErrorFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $ErrorFile) {Remove-Item -Force -Path $ErrorFile}

        &quot;=-=-=commit&quot;|Write-Debug
        $commitcomment=(&quot;RunBook-&quot;+(Get-Date -Format o).Split(&quot;+&quot;)[0])
        Start-Process -FilePath ($Using:GitWin32Path+&quot;\sh.exe&quot;) -Wait `
                -RedirectStandardOutput $OutputFile  -RedirectStandardError $ErrorFile `
                -ArgumentList (&quot;--login -c '/bin/git commit -v -m &quot;+$commitcomment+&quot;'&quot;) -WorkingDirectory $gitprojectdir
        $keepcommitinformation=Get-Content $OutputFile
        Get-Content $OutputFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;Welcome to Git*&quot; -and $_ -notlike &quot;*git help*&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $OutputFile) {Remove-Item -Force -Path $OutputFile}
        Get-Content $ErrorFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $ErrorFile) {Remove-Item -Force -Path $ErrorFile}

        &quot;=-=-=push&quot;|Write-Debug
        Start-Process -FilePath ($Using:GitWin32Path+&quot;\sh.exe&quot;) -Wait `
                -RedirectStandardOutput $OutputFile  -RedirectStandardError $ErrorFile `
                -ArgumentList &quot;--login -c '/bin/git push'&quot; -WorkingDirectory $gitprojectdir
        $keeppushinformation=Get-Content $OutputFile
        Get-Content $OutputFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;Welcome to Git*&quot; -and $_ -notlike &quot;*git help*&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $OutputFile) {Remove-Item -Force -Path $OutputFile}
        Get-Content $ErrorFile|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;#*&quot;}|Write-Debug;if (Test-Path $ErrorFile) {Remove-Item -Force -Path $ErrorFile}

        # === BEGIN RUNBOOK OUTPUT 2nd ===
        # The output for RunBook of what &quot;git commit&quot; and &quot;git push&quot; actually did
        $keepcommitinformation|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;Welcome to Git*&quot; -and $_ -notlike &quot;*git help*&quot; -and $_ -notlike &quot;#*&quot;}
        $keeppushinformation|where {$_ -ne &quot;&quot; -and $_ -notlike &quot;Welcome to Git*&quot; -and $_ -notlike &quot;*git help*&quot; -and $_ -notlike &quot;#*&quot;}
        # === END RUNBOOK OUTPUT 2nd ===

        # Cleanup (identical to Cleanup in error/exit section above)
        if (Test-Path $workdir) {Remove-Item -Force -Recurse -Path $workdir}
        if (Test-Path $sshdir) {Remove-Item -Force -Recurse -Path $sshdir}
        if (Test-Path ($Env:USERPROFILE+&quot;\.gitconfig&quot;)) {
            Remove-Item -Force -Recurse -Path ($Env:USERPROFILE+&quot;\.gitconfig&quot;)}
        if (Test-Path ($Env:USERPROFILE+&quot;\.bash_history&quot;))  {
            Remove-Item -Force -Recurse -Path ($Env:USERPROFILE+&quot;\.bash_history&quot;)}
        if (Test-Path $OutputFile) {Remove-Item -Force -Path $OutputFile}
        if (Test-Path $ErrorFile) {Remove-Item -Force -Path $ErrorFile}
        [System.Environment]::SetEnvironmentVariable(&quot;PATH&quot;,$ExistingUserpath,&quot;User&quot;)
        [System.Environment]::SetEnvironmentVariable(&quot;HOME&quot;,$null,&quot;User&quot;)
        [System.Environment]::SetEnvironmentVariable(&quot;USERPROFILE&quot;,$null,&quot;User&quot;)
    }
}