Deploying Windows Defender updates with WSUS

Context:

I’ve been recently alerted by a Nessus report that Windows Defender wasn’t up-to-date. Nessus actually raised the following two alerts about the Malware Protection Engine in various products:

The Nessus database references the issues on these pages:

And both Nessus reports link to http://support.microsoft.com/kb/2510781, which says:

Note Windows Defender may be disabled when Microsoft Security Essentials (MSE) or Forefront Endpoint Protection (FEP) is being installed. This is by design, as MSE and FEP are functional supersets of Windows Defender. The currently active product will receive engine and definition updates accordingly.

In other words, just deploying the latest Definition Update for whatever installed Malware Protection Engine would fix these two Nessus alerts :-D

Hands-on! Update the WSUS server configuration

Note that the following will run on a WSUS server built-in Windows 2012 R2.

# View what are the currently selected products
(Get-WsusServer).GetSubscription().GetUpdateCategories() | 
Format-Table Title,Id -AutoSize

NB: One cannot just add new products or classifications because there’s no method for this purpose :-(

# Select the products I want and update the WSUS config
$subscription = (Get-WsusServer).GetSubscription()
$products = (Get-WsusServer).GetUpdateCategories() | Where {
    $_.Id -in @(
        'bfe5b177-a086-47a0-b102-097e4fa1f807', # Windows 7
        '6407468e-edc7-4ecd-8c32-521f64cee65e', # Windows 8.1
        '8c3fcc84-7410-4a95-8b89-a166a0190486'  # Windows Defender
    )
}
$coll = New-Object -TypeName Microsoft.UpdateServices.Administration.UpdateCategoryCollection
$products | foreach { $coll.Add($_) }
$subscription.SetUpdateCategories($coll)
$subscription.Save()
# View what are the currently selected classifications
(Get-WsusServer).GetSubscription().GetUpdateCategories() |
Format-Table Title,Id -AutoSize
# Select the classifications I want and update the WSUS config
$subscription = (Get-WsusServer).GetSubscription()
$classifications = (Get-WsusServer).GetUpdateClassifications() | Where {
    $_.Id -in @(
        'e6cf1350-c01b-414d-a61f-263d14d133b4', # Critical Updates
        '0fa1201d-4330-4fa8-8ae9-b877473b6441', # Security Updates
        '68c5b0a3-d1a6-4553-ae49-01d3a7827828', # Service Packs
        'e0789628-ce08-4437-be74-2495b842f43b'  # Definition Updates 
    )
}
$coll = New-Object -TypeName Microsoft.UpdateServices.Administration.UpdateClassificationCollection
$classifications  | foreach { $coll.Add($_) }
$subscription.SetUpdateClassifications($coll)
$subscription.Save()

Deploy the Definition updates

First the WSUS needs to be re-synced from its upstream source (Microsoft Update, in my case)

(Get-WsusServer).GetSubscription().StartSynchronization()

Whatever the speed of your ISP link, go and get a coffee or your favorite beverage.
When you’re back, check the progress of the sync.

(Get-WsusServer).GetSubscription().GetSynchronizationStatus()   

…or if it finished, check it’s status

(Get-WsusServer).GetSubscription().GetLastSynchronizationInfo()

There are two methods to deploy definition updates either by using an ADR (Automatic Deployment Rule) or manually. The ADR method is documented on this page KB919772

But let’s do it manually as I don’t like ADR :-P

Let’s figure out what needs to be deployed:

(Get-WsusServer).SearchUpdates("Defender") | 
Where { 
    $_.PublicationState -ne 'Expired' -and
    -not($_.isSuperseded) } | 
Sort-Object CreationDate | 
Select -Last 2 | 
Format-Table Title,is* -AutoSize

Humm.. there are two different KB. KB915597 is for Windows 7/2008R2 and KB2267602 (that leads to a Oops!! page currently) for Windows 8.1/2012R2

# Select my target group: Windows 7 computers
$targetgroup = (Get-WsusServer).GetComputerTargetGroups() | 
Where Name -eq "Windows 7 x64"

# Make sure nothing is approved
# or mark as 'notapproved' any previously approved Defender updates
# from last month
(Get-WsusServer).SearchUpdates("Defender") | 
Where isApproved | ForEach-Object -Process {            
  $_.Approve(            
[Microsoft.UpdateServices.Administration.UpdateApprovalAction]::NotApproved,            
   $targetgroup            
  )            
 }

# Approve the latest definition update for Windows 7
(Get-WsusServer).SearchUpdates("Defender") | 
Where { 
    $_.PublicationState -ne 'Expired' -and  
    -not($_.isSuperseded)  -and
    ($_.Title -match "915597")
} |
Sort-Object CreationDate | 
Select -Last 1 | foreach {
 $_.Approve(            
[Microsoft.UpdateServices.Administration.UpdateApprovalAction]::Install,            
  $targetgroup            
 )
}

PowerShell and WSUS rocks! No doubt 8-)

Troubleshooting the client’s current Active Directory site

I’ve seen a nice tweet a few weeks ago about how to get the client’s current Active Directory Site.

I cannot remember who tweeted it. Kudos to him/her, it’s very handy.

[DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite(
).Name

Of course, there are other (ugly) ways to get it, using gpresult.exe

gpresult /R /scope computer| 
Select-String -Pattern "Site\sName" |
Out-String | 
ConvertFrom-Csv -Delimiter ":" -Header "Property","Value"

… or using nltest.exe

((nltest /DSGETSITE) -split "`n")[0]

The .Net way is now definetly my favorite :-D

I wondered how far this System.DirectoryServices.ActiveDirectory.ActiveDirectorySite .Net class is available?

I could have find the answer using the following MSDN page:

…but my first attempt was the hard way :\
I remembered that I could easily get the Dll a cmdlet is loaded from with the Get-Command cmdlet:

Get-Command -Name Get-Command | fl Dll


How can I get the same info for a .Net class?

 [System.Reflection.Assembly]::GetAssembly(
 [System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]
 ) | fl Location

So, the answer is that the System.DirectoryServices.ActiveDirectory.ActiveDirectorySite .Net class is available as of .Net version 2.0 on any (domain joined) client. Cool, isn’t it? 8-)

Quick tip: subnet and ConfigMgr boundary for Direct Access clients

I’ve seen this morning the following blog post about boundaries in Configuration Manager for Direct Access clients.

I’d like to add more info on this topic because I’ve done the same in my environment a few days ago.

Gerry Hampson shows that he gets the ipv6 prefix directly in the properties of a Configuration Managment client in the ConfigMgr Admin console.
He made here an assumption. Having IPv6 addresses reported in Configuration Manager assumes that you have an IPv6 based DNS server where AAAA records are created for your Direct Access clients.
When you don’t have an IPv6 DNS server, you don’t have this info in the ConfigMgr client properties:

Where do I get the client ipv6 prefix ?

You can get it on your Direct Access server(s) with the following cmdlet

Get-RemoteAccess | select  ClientIPv6Prefix

Easy, isn’t it? 8-)

Don’t forget! It may seem pretty obvious but…
Before adding the IPv6 prefix as a ConfigMgr boundary, add it first to your Active Directory Sites and Services subnets.
This way your Direct Access clients will immediately know with what Global Catalog and Domain controllers they should talk to.

Fix the infamous 0x800f0906 or ‘the source files could not be downloaded’ error

In September 2014, Microsoft released MS14-046 that would prevent you from enabling the .Net Framework 3.5 on Windows Server 2012 R2, 2012, Windows 8 and 8.1.

It was reported by this blog post:

http://blogs.technet.com/b/askpfeplat/archive/2014/09/29/attempting-to-install-net-framework-3-5-on-windows-server-2012-r2-fails-with-error-code-0x800f0906-or-the-source-files-could-not-be-downloaded-even-when-supplying-source.aspx

…and was recently updated to tell us that there’s a hotfix available now on http://support2.microsoft.com/kb/3005628

What’s the problem?

You cannot enable the .Net Framework either from Windows Update source or any other local sources (WSUS, the Sources\SXS folder of your Windows ISO image).
And you get the infamous 0x800f0906 or ‘the source files could not be downloaded’ error :-(

What caused it

How to fix the problem (official guidance)

At the end of the blog post, there’s an extra recommendation you could follow. (Remember, it’s not the first time that we get the 0x800f0906 error when enabling .Net Framework3.5)

Let’s see some PowerShell based tips to see how to troubleshoot and fix this :-)

Is .Net 3.5 installed on Windows Server 2012 R2 ?

Get-WindowsFeature | Where Name -match "Net-Frame"

Is .Net 3.5 installed on Windows 8.1 ?

Get-WindowsOptionalFeature -Online | 
Where FeatureName -match "^NetFx(3|4-)"

Is the offending MS14-046 update installed on my system ?

# For Windows 2012R2 and Windows 8.1
Get-HotFix | Where HotfixID -match  "2966828"
# For Windows 2012 and Windows 8
Get-HotFix | Where HotfixID -match  "2966827"

If there’s no result, it means that it’s not installed (good news).

How can I reproduce the issue on my Windows Server 2012 R2 ?

# Either install from local sources\sxs folder
Mount-DiskImage -ImagePath .\en_windows_server_2012_r2_vl_with_update_x64_dvd_4065221.iso
dir D:\
Install-WindowsFeature -Name NET-Framework-Core -Source D:\sources\sxs -Restart:$false -Verbose
# or from Windows Update
Install-WindowsFeature -Name NET-Framework-Core -Source "Windows Update" -Restart:$false -Verbose

What can I see in the logs

"dism","cbs" | foreach {
 sls -Pattern "0x800f090(e|6)" -Path "$($env:systemroot)\logs\$($_)\$($_).log"
}

NB: sls is the alias of the select-string cmdlet (grep for Windows :-D )

Uninstall the offending update using wusa.exe

 wusa /uninstall /kb:2966828 /quiet /norestart
Get-WinEvent -MaxEvents 3 -LogName Setup |
Select -Expand Message


NB: no reboot is required :-)

Uninstall the offending update using DISM cmdlets

Get-WindowsPackage -Online |
Where 'PackageName' -match "2966828" |
Remove-WindowsPackage -Online -Verbose -NoRestart

Next steps?

  • Enable the .Net Framework 3.5 feature
  • # Install from local sources\sxs folder
    Install-WindowsFeature -Name NET-Framework-Core -Source D:\sources\sxs -Restart:$false -Verbose
    

  • Install all security updates required for .Net Framework 3.5 including the offending update previously uninstalled
  • $UpdatesFromMU = Get-WindowsUpdate -FromMU:$true
    $UpdatesFromMU | Select Title,@{
        l='Category';e={$_.Categories | Where { -not($_.Parent) } | Select -Expand Name}
    } | Where Category -match "Security" | Out-GridView
    

    NB: I’ve used the Get-WindowsUpdate function from this post

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'