Follow-up on downloading Windows Assessment and Deployment Kit (Windows ADK) 8.1 (September 2014)

There’s a new release of the Windows Assessment and Deployment Kit (Windows ADK) 8.1 this month.
I’ve seen a blog post from Johan Arwidmark about this new release:

http://www.deploymentresearch.com/Research/tabid/62/EntryId/187/What-rsquo-s-new-in-the-4th-release-of-Windows-ADK-8-1.aspx

I just came back from holidays and have prepared the updated code before my holidays but hadn’t time to blog about it :-|

What did I change in the code? Not many things.
I’ve removed duplicate entries in the here-strings and I’m now using a $PatchLevel variable to keep track of the current version.

#Requires -Version 4
#Requires -RunAsAdministrator 
   
Function Get-ADKFiles {
[CmdletBinding()]    
param(
    [parameter(Mandatory)]
    [system.string]$TargetFolder
)
Begin {
    $HT = @{}
    $HT += @{ ErrorAction = 'Stop'}
    # Validate target folder
    try {
        Get-Item $TargetFolder @HT | Out-Null
    } catch {
        Write-Warning -Message "The target folder specified as parameter does not exist"
        break
    }

    # April 2014: 8.100.26629
    $PatchLevel = "8.100.26866"

}
   
Process {
    $adkGenericURL = (Invoke-WebRequest -Uri http://go.microsoft.com/fwlink/?LinkID=313081 -MaximumRedirection 0 -ErrorAction SilentlyContinue)
 
   
    # 302 = redirect as moved temporarily
    if ($adkGenericURL.StatusCode -eq 302) {
       
        # Currently set to http://download.microsoft.com/download/6/A/E/6AEA92B0-A412-4622-983E-5B305D2EBE56/adk/
        $MainURL = $adkGenericURL.Headers.Location
   
        $InstallerURLs = DATA {
            ConvertFrom-StringData @'
                0=0302dc615b0a5fd4810430b2cdacb5e3.cab
                1=036c618de505eeb40cca35afad6264f5.cab
                2=0708be5ffbe332f6a1571c929c1322a5.cab
                3=0a3a39d2f8a258e1dea4e76da0ec31b8.cab
                4=0b63b7c537782729483bff2d64a620fa.cab
                5=0c48c56ca00155f992c30167beb8f23d.cab
                6=0ce2876e9da7f82aac8755701aecfa64.cab
                7=0d981f062236baed075df3f42b1747db.cab
                8=11bdc4a4637c4d7ab86107fd13dcb9c6.cab
                9=125b1c8c81e36ec9dbe5abf370ff9919.cab
                10=1439dbcbd472f531c37a149237b300fc.cab
                11=1620efa4ffe2a6563530bd0158b17fe6.cab
                12=17c9d60f2bc5bc54c58782d614afcbf0.cab
                13=18e5e442fc73caa309725c0a69394a46.cab
                14=1f90b0f7321fab8dcdedaba3b30415f3.cab
                15=23ca402f61cda3f672b3081da79dab63.cab
                16=24b9e5f1f97c2f05aa95ee1f671fd3cc.cab
                17=2517aec0259281507bfb693d7d136f30.cab
                18=268b1a41f6bd2906449944b964bf7393.cab
                19=3585b51691616d290315769bec85eb6f.cab
                20=3611bd81544efa3deb061718f15aee0c.cab
                21=36e3c2de16bbebad20daec133c22acb1.cab
                22=377a2b6b26ea305c924c25cf942400d6.cab
                23=3814eaa1d4e897c02ac4ca93e7e7796a.cab
                24=388dee738d7d1c99d6fe776a85ee32f8.cab
                25=38d93b8047d5efb04cf01ab7ec66d090.cab
                26=39837d43d71c401e7edc9ba3e569cd69.cab
                27=3b71855dfae6a44ab353293c119908b8.cab
                28=3d610ba2a5a333717eea5f9db277718c.cab
                29=3dc1ed76e5648b575ed559e37a1052f0.cab
                30=3eaef6a740a72a55f4a0ac3039d05419.cab
                31=413a073d16688e177d7536cd2a64eb43.cab
                32=450f8c76ee138b1d53befd91b735652b.cab
                33=45c632fb53b95fe3bd58a6242325afa6.cab
                34=4d15138ec839ce36f5b68c16b332920a.cab
                35=4d2878f43060bacefdd6379f2dae89b0.cab
                36=4defb086385752d8cd0d1432900fb4ca.cab
                37=4e56c6c11e546d4265da4e9ff7686b67.cab
                38=4fc82a5cedaab58e43b487c17f6ef6f3.cab
                39=500e0afd7cc09e1e1d6daca01bc67430.cab
                40=527b957c06e68ebb115b41004f8e3ad0.cab
                41=56dd07dea070851064af5d29cadfac56.cab
                42=56e5d88e2c299be31ce4fc4a604cede4.cab
                43=57007192b3b38fcd019eb88b021e21cc.cab
                44=5775a15b7f297f3e705a74609cb21bbc.cab
                45=5ac1863798809c64e85c2535a27a3da6.cab
                46=5d984200acbde182fd99cbfbe9bad133.cab
                47=625aa8d1c0d2b6e8cf41c50b53868ecd.cab
                48=630e2d20d5f2abcc3403b1d7783db037.cab
                49=662ea66cc7061f8b841891eae8e3a67c.cab
                50=6894c1e1e549c4ab533078e3ff2e92af.cab
                51=690b8ac88bc08254d351654d56805aea.cab
                52=69f8595b00cf4081c2ecc89420610cbd.cab
                53=6bdcd388323175da70d836a25654aa92.cab
                54=6d2cfb2c5343c33c8d9e54e7d1f613f9.cab
                55=6d3c63e785ac9ac618ae3f1416062098.cab
                56=6da2af86cb1227e66cf9bc85f2786782.cab
                57=6dc62760f8235e462db8f91f6eaa1d90.cab
                58=7011bf2f8f7f2df2fdd2ed7c82053d7f.cab
                59=732eefaf52275b7a708311a31c82c814.cab
                60=77adc85e5c49bbd36a91bb751dc55b39.cab
                61=781e7c95c1b6b277057c9b53b7b5a044.cab
                62=7ab29d7f105f1e7814198f23b60f8e5d.cab
                63=7c11b295fb7f25c6d684b1957e96a226.cab
                64=7c195d91008a0a6ad16e535ac228467d.cab
                65=83bd1072721871ea0bdc4fab780d9382.cab
                66=8624feeaa6661d6216b5f27da0e30f65.cab
                67=86ae476dfe0498a5b5d1b6f3076412c7.cab
                68=870d7f92116bc55f7f72e7a9f5d5d6e1.cab
                69=8c27542f7954c25af62730fbb1e211d2.cab
                70=9050f238beb90c3f2db4a387654fec4b.cab
                71=93ed81ef8cf2e77c6ebc8aba5d95b9cf.cab
                72=94cae441bc5628e21814208a973bbb9d.cab
                73=9722214af0ab8aa9dffb6cfdafd937b7.cab
                74=a011a13d3157dae2dbdaa7090daa6acb.cab
                75=a03686381bcfa98a14e9c579f7784def.cab
                76=a1d26d38d4197f7873a8da3a26fc351c.cab
                77=a30d7a714f70ca6aa1a76302010d7914.cab
                78=a32918368eba6a062aaaaf73e3618131.cab
                79=a565f18707816c0d052281154b768ac0.cab
                80=a7eb3390a15bcd2c80a978c75f2dcc4f.cab
                81=aa25d18a5fcce134b0b89fb003ec99ff.cab
                82=aa4db181ead2227e76a3d291da71a672.cab
                83=ab3291752bc7a02f158066789e9b0c03.cab
                84=abbeaf25720d61b6b6339ada72bdd038.cab
                85=ac9ff098e23012b74624db792b538132.cab
                86=Application Compatibility Toolkit-x64_en-us.msi
                87=Application Compatibility Toolkit-x86_en-us.msi
                88=Assessments on Client-x86_en-us.msi
                89=Assessments on Server-x86_en-us.msi
                90=b0189bdfbad208b3ac765f88f21a89df.cab
                91=b3892d561b571a5b8c81d33fbe2d6d24.cab
                92=b5227bb68c3d4641d71b769e3ac606a1.cab
                93=b6758178d78e2a03e1d692660ec642bd.cab
                94=bbf55224a0290f00676ddc410f004498.cab
                95=bc1fef9daa903321722c08ce3cf51261.cab
                96=bd748d6fbff59b2a58cebdb99c3c6747.cab
                97=be7ebc1ac434ead4ab1cf36e3921b70e.cab
                98=c0f42c479da796da513cc5592f0759d3.cab
                99=c6babfeb2e1e6f814e70cacb52a0f923.cab
                100=c98a0a5b63e591b7568b5f66d64dc335.cab
                101=cd23bfdfd9e3dfa8475bf59c2c5d6901.cab
                102=cfb8342932e6752026b63046a8d93845.cab
                103=d2611745022d67cf9a7703eb131ca487.cab
                104=d519967dbb262c80060d9efb5079aa23.cab
                105=d562ae79e25b943d03fc6aa7a65f9b81.cab
                106=d5abe4833b23e13dc7038bde9c525069.cab
                107=dotNetFx45_Full_x86_x64.exe
                108=e5f4f4dc519b35948be4500a7dfeab14.cab
                109=e65f08c56c86f4e6d7e9358fa99c4c97.cab
                110=ea9c0c38594fd7df374ddfc620f4a1fd.cab
                111=eacac0698d5fa03569c86b25f90113b5.cab
                112=ed711e0a0102f1716cc073671804eb4c.cab
                113=eebe1a56de59fd5a57e26205ff825f33.cab
                114=f2a850bce4500b85f37a8aaa71cbb674.cab
                115=f480ed0b7d2f1676b4c1d5fc82dd7420.cab
                116=f7699e5a82dcf6476e5ed2d8a3507ace.cab
                117=f8f7800500b180b8a2103c40ce94f56a.cab
                118=fa7c072a4c8f9cf0f901146213ebbce7.cab
                119=fbcf182748fd71a49becc8bb8d87ba92.cab
                120=fcc051e0d61320c78cac9fe4ad56a2a2.cab
                121=fd5778f772c39c09c3dd8cd99e7f0543.cab
                122=fe43ba83b8d1e88cc4f4bfeac0850c6c.cab
                123=InstallRegHiveRecoveryDriverAmd64.exe
                124=InstallRegHiveRecoveryDriverX86.exe
                125=Kits Configuration Installer-x86_en-us.msi
                126=Microsoft Compatibility Monitor-x86_en-us.msi
                127=SQLEXPR_x86_ENU.exe
                128=Toolkit Documentation-x86_en-us.msi
                129=User State Migration Tool-x86_en-us.msi
                130=Volume Activation Management Tool-x86_en-us.msi
                131=wasinstaller.exe
                132=WimMountAdkSetupAmd64.exe
                133=WimMountAdkSetupArm.exe
                134=WimMountAdkSetupX86.exe
                135=Windows Assessment Services - Client (AMD64 Architecture Specific, Client SKU)-x86_en-us.msi
                136=Windows Assessment Services - Client (AMD64 Architecture Specific, Server SKU)-x86_en-us.msi
                137=Windows Assessment Services - Client (Client SKU)-x86_en-us.msi
                138=Windows Assessment Services - Client (Server SKU)-x86_en-us.msi
                139=Windows Assessment Services - Client (X86 Architecture Specific, Client SKU)-x86_en-us.msi
                140=Windows Assessment Services-x86_en-us.msi
                141=Windows Assessment Toolkit (AMD64 Architecture Specific)-x86_en-us.msi
                142=Windows Assessment Toolkit (X86 Architecture Specific)-x86_en-us.msi
                143=Windows Assessment Toolkit-x86_en-us.msi
                144=Windows Deployment Customizations-x86_en-us.msi
                145=Windows Deployment Tools-x86_en-us.msi
                146=Windows PE x86 x64 wims-x86_en-us.msi
                147=Windows PE x86 x64-x86_en-us.msi
                148=Windows System Image Manager on amd64-x86_en-us.msi
                149=Windows System Image Manager on x86-x86_en-us.msi
                150=WPT Redistributables-x86_en-us.msi
                151=WPTarm-arm_en-us.msi
                152=WPTx64-x86_en-us.msi
                153=WPTx86-x86_en-us.msi
'@
        }
   
        $PatchesURLs = DATA {
            ConvertFrom-StringData @'
                0=Toolkit Documentation-x86_en-us.msp
                1=Application Compatibility Toolkit-x86_en-us.msp
                2=Application Compatibility Toolkit-x64_en-us.msp
                3=Windows Deployment Tools-x86_en-us.msp
                4=Windows System Image Manager on amd64-x86_en-us.msp
                5=Windows System Image Manager on x86-x86_en-us.msp
                6=Windows PE x86 x64-x86_en-us.msp
                7=User State Migration Tool-x86_en-us.msp
                8=Volume Activation Management Tool-x86_en-us.msp
                9=WPTx86-x86_en-us.msp
                10=WPTx64-x86_en-us.msp
                11=WPT Redistributables-x86_en-us.msp
                12=Windows Assessment Toolkit-x86_en-us.msp
                13=Assessments on Client-x86_en-us.msp
                14=Windows Assessment Services-x86_en-us.msp
                15=Windows Assessment Services - Client (Server SKU)-x86_en-us.msp
                16=Assessments on Server-x86_en-us.msp
                17=Windows Assessment Services - Client (Client SKU)-x86_en-us.msp
'@
        }
  
        "Installers","Patches\$PatchLevel" | ForEach-Object -Process {
            # Create target folders if required as BIT doesn't accept missing folders
            If (-not(Test-Path (Join-Path -Path $TargetFolder -ChildPath $_))) {
                try {
                    New-Item -Path (Join-Path -Path $TargetFolder -ChildPath $_) -ItemType Directory -Force @HT
                } catch {
                    Write-Warning -Message "Failed to create folder $($TargetFolder)/$_"
                    break
                }
            }
        }
        # Get adksetup.exe
        Invoke-WebRequest -Uri "$($MainURL)adksetup.exe" -OutFile  "$($TargetFolder)\adksetup.exe"
   
        # Create a job that will downlad our first file
        $job = Start-BitsTransfer -Suspended -Source "$($MainURL)Installers/$($InstallerURLs['0'])" -Asynchronous -Destination (Join-Path -Path $TargetFolder -ChildPath ("Installers/$($InstallerURLs['0'])")) 
           
        # Downlod installers
        For ($i = 1 ; $i -lt $InstallerURLs.Count ; $i++) {
            $URL = $Destination = $null
            $URL = "$($MainURL)Installers/$($InstallerURLs[$i.ToString()])"
            $Destination = Join-Path -Path (Join-Path -Path $TargetFolder -ChildPath Installers) -ChildPath (([URI]$URL).Segments[-1] -replace '%20'," ")
            # Add-BitsFile http://technet.microsoft.com/en-us/library/dd819411.aspx
            $newjob = Add-BitsFile -BitsJob $job -Source  $URL -Destination $Destination
            Write-Progress -Activity "Adding file $($newjob.FilesTotal)" -Status "Percent completed: " -PercentComplete (($newjob.FilesTotal)*100/($InstallerURLs.Count))
        }
  
        # Donwload Patches
        For ($i = 0 ; $i -lt $PatchesURLs.Count ; $i++) {
            $URL = $Destination = $null
            $URL = "$($MainURL)Patches/$PatchLevel/$($PatchesURLs[$i.ToString()])"
            $Destination = Join-Path -Path (Join-Path -Path $TargetFolder -ChildPath "Patches/$PatchLevel") -ChildPath (([URI]$URL).Segments[-1] -replace '%20'," ")
            # Add-BitsFile http://technet.microsoft.com/en-us/library/dd819411.aspx
            $newjob = Add-BitsFile -BitsJob $job -Source  $URL -Destination $Destination
        }
   
        # Begin the download and show us the job
        Resume-BitsTransfer  -BitsJob $job -Asynchronous
   
        # http://msdn.microsoft.com/en-us/library/windows/desktop/ee663885%28v=vs.85%29.aspx
        while ($job.JobState -in @('Connecting','Transferring','Queued')) {
            Write-Progress -activity "Downloading ADK files" -Status "Percent completed: " -PercentComplete ($job.BytesTransferred*100/$job.BytesTotal)
        } 
        Switch($job.JobState) {
         "Transferred" {
            Complete-BitsTransfer -BitsJob $job
            break
        }
         "Error" {
            # List the errors.
            $job | Format-List
        } 
        default {
            # Perform corrective action.
        } 
        }
    }
}
End {}
}

PoC: Tatoo the background of your virtual machines

You may know the popular Bginfo from Sysinternals and that even Azure uses this utility to tatoo the background of virtual machines.

I wondered if PowerShell (alone) would make it and avoid the dependency on an external binary.

I started to use Google and finally decided to fork the following code available on github: https://github.com/fabriceleal/Imagify/blob/master/imagify.ps1

I also needed to find the way to set a wallpaper under Windows 7 and later…
I decided to extend this PowerTip: http://powershell.com/cs/blogs/tips/archive/2014/01/10/change-desktop-wallpaper.aspx because the rundll32 tricks doesn’t work.

I created two functions, one to create a new background image either from scratch and based on a colored theme (blue, grey and black) or from the existing wallpaper and the second one to set this image as a wallpaper.

Function New-BGinfo {
    Param(  [Parameter(Mandatory)]
            [string] $Text,

            [Parameter()]
            [string] $OutFile= "$($env:temp)\BGInfo.bmp",

            [Parameter()]
            [ValidateSet("Left","Center")]
            [string]$Align="Center",


            [Parameter()]
            [ValidateSet("Blue","Grey","Black")]
            [string]$Theme="Blue",

            [Parameter()]
            [string]$FontName="Arial",

            [Parameter()]
            [ValidateRange(9,45)]
            [int32]$FontSize = 12,

            [Parameter()]
            [switch]$UseCurrentWallpaperAsSource
    )
Begin {

    Switch ($Theme) {
        Blue {
            $BG = @(58,110,165)
            $FC1 = @(254,253,254)
            $FC2 = @(185,190,188)
            $FS1 = $FontSize+1
            $FS2 = $FontSize-2
            break
        }
        Grey {
            $BG = @(77,77,77)
            $FC1 = $FC2 = @(255,255,255)
            $FS1=$FS2=$FontSize
            break
        }
        Black {
            $BG = @(0,0,0)
            $FC1 = $FC2 = @(255,255,255)
            $FS1=$FS2=$FontSize
        }
    }
    Try {
        [system.reflection.assembly]::loadWithPartialName('system.drawing.imaging') | out-null
        [system.reflection.assembly]::loadWithPartialName('system.windows.forms') | out-null

        # Draw string > alignement
        $sFormat = new-object system.drawing.stringformat

        Switch ($Align) {
            Center {
                $sFormat.Alignment = [system.drawing.StringAlignment]::Center
                $sFormat.LineAlignment = [system.drawing.StringAlignment]::Center
                break
            }
            Left {
                $sFormat.Alignment = [system.drawing.StringAlignment]::Center
                $sFormat.LineAlignment = [system.drawing.StringAlignment]::Near
            }
        }


        if ($UseCurrentWallpaperAsSource) {
            $wpath = (Get-ItemProperty 'HKCU:\Control Panel\Desktop' -Name WallPaper -ErrorAction Stop).WallPaper
            if (Test-Path -Path $wpath -PathType Leaf) {
                $bmp = new-object system.drawing.bitmap -ArgumentList $wpath
                $image = [System.Drawing.Graphics]::FromImage($bmp)
                $SR = $bmp | Select Width,Height
            } else {
                Write-Warning -Message "Failed cannot find the current wallpaper $($wpath)"
                break
            }
        } else {
            $SR = [System.Windows.Forms.Screen]::AllScreens | Where Primary | 
            Select -ExpandProperty Bounds | Select Width,Height

            Write-Verbose -Message "Screen resolution is set to $($SR.Width)x$($SR.Height)" -Verbose

            # Create Bitmap
            $bmp = new-object system.drawing.bitmap($SR.Width,$SR.Height)
            $image = [System.Drawing.Graphics]::FromImage($bmp)
    
            $image.FillRectangle(
                (New-Object Drawing.SolidBrush (
                    [System.Drawing.Color]::FromArgb($BG[0],$BG[1],$BG[2])
                )),
                (new-object system.drawing.rectanglef(0,0,($SR.Width),($SR.Height)))
            )

        }
    } Catch {
        Write-Warning -Message "Failed to $($_.Exception.Message)"
        break
    }
}
Process {

    # Split our string as it can be multiline
    $artext = ($text -split "\r\n")
    
    $i = 1
    Try {
        for ($i ; $i -le $artext.Count ; $i++) {
            if ($i -eq 1) {
                $font1 = New-Object System.Drawing.Font($FontName,$FS1,[System.Drawing.FontStyle]::Bold)
                $Brush1 = New-Object Drawing.SolidBrush (
                    [System.Drawing.Color]::FromArgb($FC1[0],$FC1[1],$FC1[2])
                )
                $sz1 = [system.windows.forms.textrenderer]::MeasureText($artext[$i-1], $font1)
                $rect1 = New-Object System.Drawing.RectangleF (0,($sz1.Height),$SR.Width,$SR.Height)
                $image.DrawString($artext[$i-1], $font1, $brush1, $rect1, $sFormat) 
            } else {
                $font2 = New-Object System.Drawing.Font($FontName,$FS2,[System.Drawing.FontStyle]::Bold)
                $Brush2 = New-Object Drawing.SolidBrush (
                    [System.Drawing.Color]::FromArgb($FC2[0],$FC2[1],$FC2[2])
                )
                $sz2 = [system.windows.forms.textrenderer]::MeasureText($artext[$i-1], $font2)
                $rect2 = New-Object System.Drawing.RectangleF (0,($i*$FontSize*2 + $sz2.Height),$SR.Width,$SR.Height)
                $image.DrawString($artext[$i-1], $font2, $brush2, $rect2, $sFormat)
            }
        }
    } Catch {
        Write-Warning -Message "Failed to $($_.Exception.Message)"
        break
    }
}
End {   
    Try { 
        # Close Graphics
        $image.Dispose();

        # Save and close Bitmap
        $bmp.Save($OutFile, [system.drawing.imaging.imageformat]::Bmp);
        $bmp.Dispose();

        # Output our file
        Get-Item -Path $OutFile
    } Catch {
        Write-Warning -Message "Failed to $($_.Exception.Message)"
        break
    }
}

} # endof function

Function Set-Wallpaper {
    Param(
        [Parameter(Mandatory=$true)]
        $Path,
        
        [ValidateSet('Center','Stretch','Fill','Tile','Fit')]
        $Style = 'Stretch'
    )
    Try {
        if (-not ([System.Management.Automation.PSTypeName]'Wallpaper.Setter').Type) {
            Add-Type -TypeDefinition @"
            using System;
            using System.Runtime.InteropServices;
            using Microsoft.Win32;
            namespace Wallpaper {
                public enum Style : int {
                Center, Stretch, Fill, Fit, Tile
                }
                public class Setter {
                    public const int SetDesktopWallpaper = 20;
                    public const int UpdateIniFile = 0x01;
                    public const int SendWinIniChange = 0x02;
                    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
                    private static extern int SystemParametersInfo (int uAction, int uParam, string lpvParam, int fuWinIni);
                    public static void SetWallpaper ( string path, Wallpaper.Style style ) {
                        SystemParametersInfo( SetDesktopWallpaper, 0, path, UpdateIniFile | SendWinIniChange );
                        RegistryKey key = Registry.CurrentUser.OpenSubKey("Control Panel\\Desktop", true);
                        switch( style ) {
                            case Style.Tile :
                                key.SetValue(@"WallpaperStyle", "0") ; 
                                key.SetValue(@"TileWallpaper", "1") ; 
                                break;
                            case Style.Center :
                                key.SetValue(@"WallpaperStyle", "0") ; 
                                key.SetValue(@"TileWallpaper", "0") ; 
                                break;
                            case Style.Stretch :
                                key.SetValue(@"WallpaperStyle", "2") ; 
                                key.SetValue(@"TileWallpaper", "0") ;
                                break;
                            case Style.Fill :
                                key.SetValue(@"WallpaperStyle", "10") ; 
                                key.SetValue(@"TileWallpaper", "0") ; 
                                break;
                            case Style.Fit :
                                key.SetValue(@"WallpaperStyle", "6") ; 
                                key.SetValue(@"TileWallpaper", "0") ; 
                                break;
}
                        key.Close();
                    }
                }
            }
"@ -ErrorAction Stop 
            } else {
                Write-Verbose -Message "Type already loaded" -Verbose
            }
        # } Catch TYPE_ALREADY_EXISTS
        } Catch {
            Write-Warning -Message "Failed because $($_.Exception.Message)"
        }
    
    [Wallpaper.Setter]::SetWallpaper( $Path, $Style )
}

Let’s see these two functions in action.

First define some multiline text to be written in the image.

$os = Get-CimInstance Win32_OperatingSystem
($o = [pscustomobject]@{
    HostName =  $env:COMPUTERNAME
    UserName = '{0}\{1}' -f  $env:USERDOMAIN,$env:USERNAME
    'Operating System' = '{0} Service Pack {1} (build {2})' -f  $os.Caption,
    $os.ServicePackMajorVersion,$os.BuildNumber
}) | ft -AutoSize
$BootTime = (New-TimeSpan -Start $os.LastBootUpTime -End (Get-Date)).ToString()

# $t is the multiline text defined as here-string
$t = @"
$($o.HostName)
Logged on user: $($o.UserName)
$($o.'Operating System')
Uptime: $BootTime
"@
  • Exemple 1: ala Backinfo
  • $WallPaper = New-BGinfo -text $t
    Set-Wallpaper -Path $WallPaper.FullName -Style Center
    

  • Exemple 2: ala Bginfo using the current wallpaper
  • $BGHT = @{
     Text  = $t ;
     Theme = "Black" ;
     FontName = "Verdana" ;
     UseCurrentWallpaperAsSource = $true ;
    }
    $WallPaper = New-BGinfo @BGHT
    Set-Wallpaper -Path $WallPaper.FullName -Style Fill
    
    # Restore the default VM wallpaper
    Set-Wallpaper -Path "C:\Windows\Web\Wallpaper\Windows\img0.jpg" -Style Fill
    

This proof of concept based on just a few hundred lines of PowerShell proves that the dependency on Bginfo could be avoided…

Quick tips about dism

Whenever I’m working on Windows 7 and Windows 2008 R2 server, I cannot leverage the new DISM PowerShell module built-in Windows 8.x and 2012 server.

The built-in command dism.exe can display packages as a list or a table.

When I’m looking for the state of a particular package, I use the Select-String cmdlet to parse the default dism output formatted as a list.
The -Context parameter allows to display the next 4 lines after it finds the matching pattern:

dism /online /get-packages | 
Select-String -Pattern "2965788" -Context 0,4


NB: sls is the alias of Select-String as of PowerShell 3.0

Yesterday, I wanted to get the list of packages sorted by their installation time.
The default format of dism.exe by list doesn’t help.
The first step was to skip the first lines of the dism output formatted in a table.
The second step consists in transforming the dism table output into objects with the ConvertFrom-Csv cmdlet.
These two steps don’t allow to sort by “Install Time” as the ‘Install Time’ is a string and not a datetime object.
The last step kills two birds with one stone. It removes the last line “The operation completed successfully” of dism.exe and converts the ‘Install Time’ values into datetime objects.

dism /online /get-packages /format:table | 
Select-Object -Skip 12 | 
ConvertFrom-Csv -Header "PackageName","State","Release Type","Install Time" -Delimiter "|" | 
foreach {
 if ($_.PackageName -notmatch "The operation completed successfully.") {
  [PSCustomObject]@{
   PackageName = $_.PackageName ;
   State = $_.State ;
   Type = $_.'Release Type' ;
   'Install Time' = (Get-Date -Date $_.'Install Time') ;
  }
 }
} | 
Sort 'Install Time'

Clear eventlog

I’ve been testing the new Sysmon tool from Sysinternals written by Mark Russinovich and Thomas Garnier I’ve already mentioned in this post.

It has been recently updated to version 1.01.
Since I updated from version 1.0 to 1.01, events written by the previous driver aren’t readable anymore using the “general view” of eventvwr.exe:

Fortunately, the xml data can still be parsed :-)

Anyway, I wanted to clear the log after I upgraded.
The built-in Clear-EventLog doesn’t help in this case :-(

The built-in Get-WinEvent cmdlet returns a System.Diagnostics.Eventing.Reader.EventLogConfiguration .Net object that hasn’t any method to clear the content of the log.

Get-WinEvent -ListLog * | 
Where LogName -match "sysmon" | Get-Member -Force

The System.Diagnostics.Eventing.Reader.EventLogSession .Net class has however a method that can clear logs:

The first method to clear the log is by just giving a logName as parameter

(New-Object System.Diagnostics.Eventing.Reader.EventLogSession).
ClearLog("Microsoft-Windows-Sysmon/Operational")

The second method to clear the log is by passing a logName and a backupath

(New-Object System.Diagnostics.Eventing.Reader.EventLogSession).
ClearLog(
 "Microsoft-Windows-Sysmon/Operational",
 "C:\windows\temp\sysmon.evtx"
)

Both methods generate a 104 event ID in the System log.

Get-WinEvent -FilterHashtable @{ LogName = 'System' ; Id = 104} -MaxEvents 2 | 
Sort TimeCreated | Foreach {
 ([xml]($_.ToXml())).Event.UserData.LogFileCleared
}


With event 104 we can see what account cleared what log and whether it’s been saved to any BackupPath.

Scan for updates from different sources and compare results

Context: I’m working with PowerShell version 2.0 on a Windows 2008 R2 server connected to Internet, managed by an internal WSUS.

Goal: I want to perform two scans to get results from WSUS and Microsoft Update to compare them.
I just want to see Important updates and not optional updates

Reminder :

  • I’ve already shown how to quickly perform a scan and hide some updates on PowerShell Magazine, here
  • I’ve also already shown on Windows 8.x how to get updates from the Windows Store, here

Foreword: To be able to get updates from Microsoft Update, your Windows Update Agent (WUA) must ‘opt-in’.
This can be achieved with the following code:

# Opt-in to MU            
$UpdateSvc = New-Object -ComObject Microsoft.Update.ServiceManager            
$UpdateSvc.AddService2("7971f918-a847-4430-9279-4a52d1efe18d",7,"")            
$UpdateSvc.QueryServiceRegistration("7971f918-a847-4430-9279-4a52d1efe18d") | fl *            
$UpdateSvc.Services | Where { $_.Name -eq "Microsoft Update"}

Step 1: Modify the Get-WindowsUpdate function I presented on PowerShell Magazine by adding a FromMU parameter

Function Get-WindowsUpdate {
 
    [Cmdletbinding()]
    Param(
        [Parameter()]
        [system.Boolean]$FromMU=$false
    )
 
    Process {
        try {
            Write-Verbose "Getting Windows Update"
            $Session = New-Object -ComObject Microsoft.Update.Session           
            $Searcher = $Session.CreateUpdateSearcher() 
            
            if($fromMU) {
                $Searcher.ServiceID = '7971f918-a847-4430-9279-4a52d1efe18d'
                $Searcher.SearchScope =  1 # MachineOnly
                $Searcher.ServerSelection = 3 # Third Party                  
            }
            $Criteria = "IsInstalled=0 and DeploymentAction='Installation' or IsPresent=1 and DeploymentAction='Uninstallation' or IsInstalled=1 and DeploymentAction='Installation' and RebootRequired=1 or IsInstalled=0 and DeploymentAction='Uninstallation' and RebootRequired=1"           
            $SearchResult = $Searcher.Search($Criteria)          
            $SearchResult.Updates
        } catch {
            Write-Warning -Message "Failed to query Windows Update because $($_.Exception.Message)"
        }
    }
}

Only 3 properties of the Searcher object have been defined inside the if/else block.
These properties can also be discovered by reading the WindowsUpdate.log.

The modified function can be tested like this:

Get-WindowsUpdate | Out-GridView
Get-WindowsUpdate -FromMU:$true | Out-GridView

Problem: The MU scan mixes both Important and optional updates although the WUA is configured like this:
(do not give me recommended updates the same way I receive important updates)

Notice also that there’s no property from the previous Out-GridView window that would help distinguish between these “kinds” of updates.
I find also ambiguous the difference between “Optional” updates and “Recommended” updates.
To avoid confusion, let’s treat “optional” updates as updates that you can see in the “Optional” view of the Windows Update GUI.

Step 2: Find out what updates are optional vs. important

$UpdatesFromMU = Get-WindowsUpdate -FromMU:$true 
$Results = $UpdatesFromMU | ForEach-Object {
    $_ | Add-Member -MemberType ScriptProperty -Name isImportant -Value ({
        if (-not($this.BrowseOnly) -and $this.AutoSelectOnWebSites) {
            $true
        } else { 
            $false
        }
    }) -Force -PassThru |
    Add-member -MemberType ScriptProperty -Name isOptional -Value ({
        -not($this.isImportant)
    }) -Force -PassThru
}

As you can see, I’ve chosen to add script properties on the fly.

Let’s check if this corresponds to an interactive scan with the GUI

$Results | Where { $_.isImportant } | Measure
$Results | Where { $_.isOptional }  |  Measure



There’s one additional optional update that is included in the scan made with PowerShell.
In my case, it’s Microsoft .NET Framework 4 for Windows Server 2008 R2 x64-based Systems (KB982671)

Step 3: Get more info about updates

This step is more experimental. As the built-in description property of the update isn’t enough descriptive (it doesn’t tell you what it fixes), I’ll use the MoreInfoUrls property and extract the title from the raw html returned by an http request.
I don’t have the handy new Invoke-WebRequest cmdlet provided by PowerShell version 3.0, so I’ll use the old method.

$Results | Where { $_.isImportant } | 
ForEach-Object { 
    $update = $_
    $TitleLine = $pageTitle = $null
    try {
        $TitleLine = (new-object Net.Webclient).DownloadString([system.Uri]$($_.MoreInfoUrls )) -split '\<' | 
        Select-string -Pattern "\:pagetitle\=" -ErrorAction Stop | Select -Expand Line
        if ($TitleLine) {
            $pageTitle = @(([regex]'.*\="(?<Title>.*)"\s/\>').Matches(($TitleLine).ToString())) | 
            Select -Last 1 -Expand Groups | 
            Select -Last 1 -Expand Value
            $_ | Add-Member -MemberType NoteProperty -Name "Page Title" -Value $pageTitle -Force -PassThru -ErrorAction Stop
        }
    } catch {
        Write-Warning -Message "Failed for $($update.Title) because $($_.Exception.Message)"
        $update | Add-Member -MemberType NoteProperty -Name "Page Title" -Value $($update.Description) -Force -PassThru
    }        
} | 
Select Title,@{
    l='Category';e={$_.Categories | Where { -not($_.Parent) } | Select -Expand Name}
},'Page Title' | 
Out-GridView

Step 4: scan for Updates from WSUS and Microsoft Update and compare results

I can now export results from the scan performed against Microsoft Update by just replacing the above Out-GridView cmdlet by the following code:

Export-Csv -Path "$($home)\Documents\scan.results.$((Get-date).toString('yyyy-MM-dd-HHmm')).MU.csv"

To get results from WSUS, I do:

#
# scan from WSUS
#
$UpdatesFromWSUS = Get-WindowsUpdate
$Results = $UpdatesFromWSUS | ForEach-Object {
    $_ | Add-Member -MemberType ScriptProperty -Name isImportant -Value ({
        if (-not($this.BrowseOnly) -and $this.AutoSelectOnWebSites) {
            $true
        } else { 
            $false
        }
    }) -Force -PassThru |
    Add-member -MemberType ScriptProperty -Name isOptional -Value ({
        -not($this.isImportant)
    }) -Force -PassThru
}
$Results | Where { $_.isImportant } | Measure
$Results | Where { $_.isOptional }  |  Measure
# > no need to distinguish between the 2 kinds :-)

$Results | 
ForEach-Object { 
    $update = $_
    $TitleLine = $pageTitle = $null
    try {
        $TitleLine = (new-object Net.Webclient).DownloadString([system.Uri]$($_.MoreInfoUrls )) -split '\<' | 
        Select-string -Pattern "\:pagetitle\=" -ErrorAction Stop | Select -Expand Line
        if ($TitleLine) {
            $pageTitle = @(([regex]'.*\="(?<Title>.*)"\s/\>').Matches(($TitleLine).ToString())) | 
            Select -Last 1 -Expand Groups | 
            Select -Last 1 -Expand Value
            $_ | Add-Member -MemberType NoteProperty -Name "Page Title" -Value $pageTitle -Force -PassThru -ErrorAction Stop
        }
    } catch {
        Write-Warning -Message "Failed for $($update.Title) because $($_.Exception.Message)"
        $update | Add-Member -MemberType NoteProperty -Name "Page Title" -Value $($update.Description) -Force -PassThru
    }        
} | 
Select Title,@{
    l='Category';e={$_.Categories | Where { -not($_.Parent) } | Select -Expand Name}
},'Page Title' | 
Export-Csv -Path "$($home)\Documents\scan.results.$((Get-date).toString('yyyy-MM-dd-HHmm')).WSUS.csv"

To compare results, I do:

Compare-Object -Property Title -IncludeEqual -ReferenceObject (
    Import-CSV (
        Get-ChildItem "$($home)\Documents\scan.results.*.MU.csv" | 
        Sort LastWriteTime | Select -Last 1
    )
) -DifferenceObject (
    Import-CSV (
        Get-ChildItem "$($home)\Documents\scan.results.*.WSUS.csv" | 
        Sort LastWriteTime | Select -Last 1
    )
) 

Enjoy :-D

about WSUS reporting

Before we start, may I introduce the official Terminology for Update Status
As you can see, the status “Needed” and “Installed/Not Applicable”, don’t mean the same thing when it applies either to a single computer or a computer target group. Don’t let the GUI fool you!

This article from the WSUS team in 2008 said that:

Usually there are two kinds of reports people run: The default WSUS reports accessed from the WSUS MMC console and the reporting tools located in the API Samples and Tools:
WSUS3: http://download.microsoft.com/download/5/d/c/5dc98401-bb01-44e7-8533-3e79ae0e0f97/Update%20Services%203.0%20API%20Samples%20and%20Tools.EXE

I’ve a Windows Server 2012 R2 core edition where I don’t want to install the Report Viewer.

I’m stuck with the second option and almost forced into using the API samples and tools ;-)

What are these tools ?

Before I go through the above list, you should know that things have changed since 2008.
A common best practice and approach in PowerShell is to return objects and let you choose the format you want to export: CSV, XML,…

I’ll now focus on showing how to achieve what the API samples and tools do with PowerShell.

  • ApprovedUpdatesToXML
    # ApprovedUpdatestoXML
    # \Update Services 3.0 API Samples and Tools\ApprovedUpdatesToXML
    
    $updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
    $updateScope.ApprovedStates = (
    ([Microsoft.UpdateServices.Administration.ApprovedStates]::HasStaleUpdateApprovals.value__+
    [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved.value__)
    )
    
    # get the list of updates that are approved, or have an older revision that is approved
    $Updates = (Get-WsusServer).GetUpdates($updateScope) 
    # calling IUpdate.GetUpdateApprovals once for each update can be expensive, so instead
    # we use call IUpdateServer.GetUpdateApprovals() to get all approvals for all updates at once
    $allUpdateApprovals = (Get-WsusServer).GetUpdateApprovals($updateScope)
    # similarly, construct a table of target group
    $allTargetGroups = (Get-WsusServer).GetComputerTargetGroups()
    $Updates | ForEach-Object {
        $update = $_
        [PSCustomObject]@{
            Id = $update.Id.UpdateId.ToString() ;
            Title = $update.Title ;
            Classification = $update.UpdateClassificationTitle ;
            Approvals = $( 
            if ($update.IsApproved) {
                # This revision (the latest revision) is approved; get the approvals and write them
                $allUpdateApprovals | Where { 
                    $_.UpdateID.UpdateID -eq $update.Id.UpdateId
                } | 
                ForEach-Object {
                    $approval = $_
                    [PSCustomObject]@{
                        RevisionNumber = $update.Id.RevisionNumber ;
                        TargetGroup = ($allTargetGroups | Where { $_.ID -eq $approval.ComputerTargetGroupId }).Name ;
                        Approval = $approval.Action ;
                        Deadline = $( 
                            if ($_.Deadline -lt ([datetime]::maxvalue)) {
                                $_.Deadline    
                            } else {
                                "None"
                            }
                        ) ;
                        ApprovalDate = $approval.CreationDate ;
                    }
                }
            } elseif ($update.HasStaleUpdateApprovals) {
                # This revision has older revisions that are approved; get their approvals and write them
                $update.GetRelatedUpdates(
                    [Microsoft.UpdateServices.Administration.UpdateRelationship]::AllRevisionsOfThisUpdate
                    ) | Where isApproved | 
                    ForEach-Object {
                    $revision = $_
                    $revision.GetUpdateApprovals() |
                    ForEach-Object {
                        $approval = $_
                        [PSCustomObject]@{
                            RevisionNumber = $update.Id.RevisionNumber ;
                            TargetGroup = ($allTargetGroups | Where { $_.ID -eq $approval.ComputerTargetGroupId }).Name ;
                            Approval = $approval.Action ;
                            Deadline = $( 
                                if ($_.Deadline -lt ([datetime]::maxvalue)) {
                                    $_.Deadline    
                                } else {
                                    "None"
                                }
                            ) ;
                            ApprovalDate = $approval.CreationDate ;
                        }
                    }
                    }
            } else {
            }
        ) ;
        }
    }  | Export-Clixml -Depth 2 -Path $home\Documents\ApprovedUpdatesToXML.xml
    
  • ComputerStatusToXML
  • # ComputerStatusToXML
    # \Update Services 3.0 API Samples and Tools\ComputerStatusToXML
    
    $computerScope = New-object Microsoft.UpdateServices.Administration.ComputerTargetScope
    $computerScope.IncludeDownstreamComputerTargets = $true
    (Get-WsusServer).GetComputerTargets($computerScope) | ForEach-Object {
        $computer = $_
        [PSCustomObject]@{
            Name = $computer.FullDomainName ;
            LastReportedStatus = $computer.LastReportedStatusTime ;
            ParentServer = $(
                if ($computer.ParentServerId -ne [System.Guid]::Empty) {
                    $computer.GetParentServer().FullDomainName
                } else {
                    'localhost'
                }
            );
            UpdateStatus = $(
                $computer.GetUpdateInstallationInfoPerUpdate() | 
                Where {
                    $_.UpdateInstallationState -ne 'NotApplicable'
                } |
                ForEach-Object {
                    [PSCustomObject]@{
                        Title = $_.GetUpdate().Title ;
                        Status = $_.UpdateInstallationState ;
                    }
                }
            );
            }
    } | 
    Export-Clixml -Depth 2 -Path $home\Documents\ComputerStatusToXML.xml
    
  • ListApprovedUpdates
  • # \Update Services 3.0 API Samples and Tools\ListApprovedUpdates
    
    $updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
    $updateScope.ApprovedStates = (
    ([Microsoft.UpdateServices.Administration.ApprovedStates]::HasStaleUpdateApprovals.value__+
    [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved.value__)
    )
    
    (Get-WsusServer).GetUpdates($updateScope) | ForEach-Object {
        $update = $_ 
        if ($_.isApproved) {
            $_.GetUpdateApprovals() | ForEach-Object {
                $approval =  $_
                [PSCustomObject]@{
                    Title = $update.Title ;
                    Classification = $update.UpdateClassificationTitle ;
                    'Applies to' = $update.ProductTitles
                    'Approved on' = $approval.CreationDate
                }
                
            }
        }
    }
    
  • UpdateStatusToCSV
  • # UpdateStatusToCSV    
    # Update Services 3.0 API Samples and Tools\UpdateStatusToCSV
    $updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
    $updateScope.ApprovedStates = (
    ([Microsoft.UpdateServices.Administration.ApprovedStates]::HasStaleUpdateApprovals.value__+
    [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved.value__)
    )
    
    (Get-WsusServer).GetUpdates($updateScope) | ForEach-Object {
        $update = $_
        $update.GetSummaryPerComputerTargetGroup() | ForEach-Object {
            if ($_.ComputerTargetGroupId.Equals([Microsoft.UpdateServices.Administration.ComputerTargetGroupId]::AllComputers)) {
                [PSCustomObject]@{
                    'Update Name' = $update.Title
                    Classification = $update.UpdateClassificationTitle
                    Installed = $_.InstalledCount
                    'Installed Pending Reboot' = $_.InstalledPendingRebootCount
                    Needed = ($_.DownloadedCount + $_.NotInstalledCount)
                    'Not Needed' = $_.NotApplicableCount
                    Failed = $_.FailedCount
                    Unknown = $_.UnknownCount
                    'Last Updated' = $(
                        switch ($_.LastUpdated) {
                            ([datetime]::MinValue) {
                                "Never" ; break
                            }
                            default {$_.ToShortDateString()}
                        })
                }
            }
        }
    } | Export-Csv -Path $home\Documents\ApprovedUpdateStatus.csv
    
  • UpdateStatustoXML
  • # UpdateStatustoXML
    # Update Services 3.0 API Samples and Tools\UpdateStatusToXML
    
    $Updates = (Get-WsusServer).GetUpdates()
    $AllComputersGroup = (Get-WsusServer).GetComputerTargetGroup(
    [Microsoft.UpdateServices.Administration.ComputerTargetGroupId]::AllComputers
    )
    
    $Updates | Where {-not($_.IsDeclined)} | ForEach-Object -Process {
        [PSCustomObject]@{
            Title = $_.Title
            ComputerStatus = ($AllComputersGroup.GetUpdateInstallationInfoPerComputerTarget($_) | ForEach-Object {
                [pscustomobject]@{
                    ComputerName = $_.GetComputerTarget().FullDomainName ;
                    InstallationStatus = $_.UpdateInstallationState.ToString() ;
                    ApprovalStatus = $_.UpdateApprovalAction.ToString()
    
                }
            })
        }
    } | Export-Clixml -Depth 2 -Path $home\Documents\ApprovedUpdateStatus.xml
    
    
  • UpdatesToXML
  • # UpdatesToXML
    # Update Services 3.0 API Samples and Tools\UpdatesToXML
    
    (Get-WsusServer).GetUpdates(
        [Microsoft.UpdateServices.Administration.ApprovedStates]::Any,
        [datetime]::MinValue,
        [datetime]::MaxValue,
        $null,
        $null
    ) | ForEach-Object -Process {
        $_ | 
        Add-Member -MemberType ScriptProperty -Name UpdateID -Value {
            $this.Id.UpdateID.toString().ToUpper()
        } -Force -PassThru |
        Add-Member -MemberType ScriptProperty -Name BundledUpdates -Value { 
                $_.GetRelatedUpdates(
                    [Microsoft.UpdateServices.Administration.UpdateRelationship]::UpdatesBundledByThisUpdate
                ) | 
                Select -Property @{l='UpdateID';e={$_.Id.UpdateID.ToString().ToUpper()}},
                Title,@{l='Classification';e={$_.UpdateClassificationTitle}}
        } -Force -PassThru | 
        Add-Member -MemberType ScriptProperty -Name BundlingUpdates -Value {
                $_.GetRelatedUpdates(
                    [Microsoft.UpdateServices.Administration.UpdateRelationship]::UpdatesThatBundleThisUpdate
                ) | 
                Select -Property @{l='UpdateID';e={$_.Id.UpdateID.ToString().ToUpper()}},
                Title,@{l='Classification';e={$_.UpdateClassificationTitle}}
        } -Force -PassThru | 
        Add-Member -MemberType ScriptProperty -Name SupersededUpdates -Value {
                $_.GetRelatedUpdates(
                    [Microsoft.UpdateServices.Administration.UpdateRelationship]::UpdatesSupersededByThisUpdate
                ) |
                Select -Property @{l='UpdateID';e={$_.Id.UpdateID.ToString().ToUpper()}},
                Title,@{l='Classification';e={$_.UpdateClassificationTitle}}
        } -Force -PassThru | 
        Add-Member -MemberType ScriptProperty -Name SupersedingUpdates -Value {
                $_.GetRelatedUpdates(
                    [Microsoft.UpdateServices.Administration.UpdateRelationship]::UpdatesThatSupersedeThisUpdate
                ) |
                Select -Property @{l='UpdateID';e={$_.Id.UpdateID.ToString().ToUpper()}},
                Title,@{l='Classification';e={$_.UpdateClassificationTitle}}
        } -Force -PassThru
        
    } | Select -Property UpdateID,Title,@{l='Classification';e={$_.UpdateClassificationTitle}},
    LegacyName,Description,@{l='SyncDate';e={$_.ArrivalDate.ToLocalTime()}},
    @{l='ReleaseDate';e={$_.CreationDate.ToLocalTime()}},MsrcSeverity,SecurityBulletins,
    @{l='KBArticles';e={$_.KnowledgeBaseArticles}},AdditionalInformationUrls,
    BundledUpdates,BundlingUpdates,SupersededUpdates,SupersedingUpdates |
    Export-Clixml -Depth 2 -Path $home\Documents\UpdatesToXML.xml
    

The above samples show that it’s possible to do some reporting under PowerShell but there are some limitations and drawbacks.

For example, UpdateStatustoXML sample is extremely slow as it will go through all updates except those that are declined.
To reduce its scope and increase performance, you should consider filtering on the left.

I was looking at WSUS reporting mainly because I wanted to determine the compliance level of the Windows 7 computers I grouped under a target group named ‘Windows 7 x64′.
How can I determine a compliance ratio?
More precisely, I wanted to know what was installed on my computer against what I approved and what may be missing again against what I previously approved.

I wanted to use the ApprovedComputerTargetGroups of the UpdateScope object to get a better filter but it’s a read-only property.

The trick that let me find my way was the updatescope used to filter both the GetUpdateApprovals and GetUpdateInstallationSummary methods.


$targetgroup = (Get-WsusServer).GetComputerTargetGroups() | Where Name -eq "Windows 7 x64"

$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.UpdateApprovalActions = "Install"
$updateScope.ApprovedStates = (
([Microsoft.UpdateServices.Administration.ApprovedStates]::HasStaleUpdateApprovals.value__+
[Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved.value__)
)
$total = (Get-WsusServer).GetUpdateApprovals($updateScope).Count
$targetgroup.GetComputerTargets($true) | Where FullDomainName -match "^MyComputerNamePattern" | 
ForEach-Object {
    $computer = $_
    $State = $computer.GetUpdateInstallationSummary($updateScope)
    [PSCustomObject]@{
        Name = $_.FullDomainName ;
        IPAddress = $_.IPAddress ;
        # 'Operating System'  = $_.OSDescription ;
        Version = $_.ClientVersion ;
        RebootPending = $(
            if($State.InstalledPendingRebootCount) {
                $true
            } else {
                $false
            }
        );
        Compliant = '{0:P}' -f (($State.InstalledCount)/($total - $State.NotApplicableCount));
        Needed = $State.FailedCount + $State.DownloadedCount + $State.UnknownCount + $_.NotInstalled
    }
}


As you can see above, I’ve got I computer that misses 15 updates. It’s expected as it’s mainboard is out-of-order…

Please note that there’s also a way to do this directly from the database.
The SQL path is available on http://blogs.technet.com/b/wsus/archive/2008/06/20/baseline-compliance-report-using-public-wsus-views.aspx

If you’re looking for the Microsoft.UpdateServices.Administration Namespace documentation it’s available on MSDN.

There are other samples available online. There’s for example: How to Determine All Approved Updates.

I’d like also to reference the great work Boe Prox already did about WSUS reporting:

As we are today the second Tuesday of August, may I wish you happy Patch Tuesday and reboot Wednesday :-D

Post-mortem #DFIR investigation of #PowerShell attacks

A nice white paper has been published by FireEye recently, entitled “Investigating PowerShell Attacks” written by Ryan Kazanciyan and Matt Hastings.
It’s actually the white paper they announced in the article they published in the two-week Security series on PowerShell Magazine last month, also entitled “Investigating PowerShell Attacks

First, I’d like to applaud the initiative and the awesome work they did :-D
It’s a good reading that summarizes what artifacts a DFIR analyst may find during a post-mortem investigation:

  • Registry
  • Prefetch
  • Network Traffic
  • Memory
  • Eventlogs
  • Persistence

If you’re not familiar with the Prefetch feature, you can read the following pages:

A side note: please bear in mind that the Prefetcher is turned off by default if the OS is running Windows 7 on a Solid State Drive (SSD).

If you’re not familiar with the “Remoting” feature of Windows PowerShell, I’d recommend reading the “Secrets of PowerShell Remoting” free ebook published by PowerShell.org and written by Don Jones, Tobias Weltner and Dave Wyatt.
While not referenced by the “Investigating PowerShell Attacks” white paper, if you want to explore how to read the data chunks in the WinRM analytic eventlog, you should read the “Diagnostics and Troubleshooting” chapter in the “Secrets of PowerShell Remoting” ebook where the authors provided and showed how to use the Construct-PSDataRemoteObject cmdlet. This cmdlet is available from the PSdiagnostics.zip file hosted on http://www.concentratedtech.com/downloads

The “Investigating PowerShell Attacks” white paper is available from

There’s one technique Ryan Kazanciyan and Matt Hastings talked about that could give additional forensics evidence:

The authors have worked with several organizations that have implemented homegrown logging solutions. One such technique entails overwriting PowerShell’s built-in Prompt function22 (again, through the addition of code in all user profiles). A custom prompt could capture any input submitted at the local PowerShell command line and save it to a file or to an event log (using the Write-EventLog cmdlet). Once again, this approach would not capture remoting activity.

That’s a great idea :-) Unfortunately, they didn’t show what the code may look like.
James O’Neill already shared some code on his blog about this technique http://jamesone111.wordpress.com/2012/01/28/adding-persistent-history-to-powershell/

More recently a PowerTip that explores other techniques was also published on this page http://powershell.com/cs/blogs/tips/archive/2014/08/08/logging-what-a-script-does.aspx It uses the Start-Transcript and the Set-PSDebug cmdlets

As we are in August, I’d like to mention that Mark Russinovich and Thomas Garnier released the Sysmon tool

System Monitor (Sysmon) is a Windows system service and device driver that, once installed on a system, remains resident across system reboots to monitor and log system activity to the Windows event log. It provides detailed information about process creations, network connections, and changes to file creation time

Source and more on http://technet.microsoft.com/en-us/sysinternals/dn798348

My fellow Windows PowerShell MVP Carlos Perez already wrote an awesome post about this new Sysmon tool that shows how useful it can be for DFIR post-mortem analysis http://www.darkoperator.com/blog/2014/8/8/sysinternals-sysmon