Waiting for BITS downloads

I was preparing a personal laptop and had to update Windows 8 Pro in French using the Windows Store.
Once the first updates have been installed, I launched the upgrade to Windows 8.1 by executing ms-windows-store:WindowsUpgrade inside the Run window launched with the combination of the Windows key + R


The Windows store displays the following progress while downloading Windows 8.1


I’d like to see a percent of how much was downloaded so far. PowerShell to the rescue!
The old DOS command bitsadmin doesn’t help and you may even notice the message telling us that it’s deprecated and that we should use PowerShell instead.

The equivalent of the above DOS command in PowerShell is:

Get-BitsTransfer -AllUsers 


Now to get the progress of all the background intelligent jobs, I need to sum up the bytes transferred and the total bytes to be downloaded of each job.

while (Get-BitsTransfer -AllUsers | Where TransferType -eq "Download") {
	$transferred = Get-BitsTransfer -AllUsers | Measure-Object -Property BytesTransferred -Sum;
	$total = Get-BitsTransfer -AllUsers | Measure-Object -Property BytesTotal -Sum ;
	Write-Progress -Activity "Download" -Status ('{0:N2}' -f (($transferred.Sum/$total.Sum)*100)) ;
} 

The only drawback I know for the BITS module is that it cannot be used inside a remote session 😦
If I do:

Invoke-Command -ScriptBlock {
	Import-Module BitsTransfer
	Get-BitsTransfer -AllUsers
} -ComputerName remotePC

I’ve the following warning:

The remote use of BITS is not supported. For more information about BITS, see the MSDN documentation at http://go.microsoft.com/FWLINK/?LinkId=140888.
+ CategoryInfo : InvalidOperation: (:) [Get-BitsTransfer], InvalidOperationException
+ FullyQualifiedErrorId : GetBitsTransferRemoteOpException,Microsoft.BackgroundIntelligentTransfer.Management.GetBitsTransfer

+ CategoryInfo : NotSpecified: (:) [Get-BitsTransfer], Exception
+ FullyQualifiedErrorId : System.Exception,Microsoft.BackgroundIntelligentTransfer.Management.GetBitsTransfer

Script Browser for Windows PowerShell ISE

The Script Browser for Windows PowerShell ISE has been announced yesterday on the PowerShell Team blog and was actually released the day before on the download center

I wanted to give it a try before the announcement but I was too enthusiast and failed to meet the requirements.
To save your time, here are the requirements. You need:

  1. Microsoft .NET Framework 4 (required by PowerShell 3.0)
  2. Windows Management Framework 3.0
  3. Microsoft .NET Framework version 4.5 or a later version (required by ScriptBrowser.dll)

I missed the .Net 4.5 and got the following error:
Add-Type : Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.


To get the above display, I modified the code of the Powershell ISE profile ($profile = “$env:USERPROFILE\Documents\WindowsPowerShell\Microsoft.PowerShellISE_profile.ps1”) like this:

#Script Browser Begin
try {
	Add-Type -Path 'C:\Program Files (x86)\Microsoft Corporation\Microsoft Script Browser\System.Windows.Interactivity.dll' -ErrorAction Stop
	Add-Type -Path 'C:\Program Files (x86)\Microsoft Corporation\Microsoft Script Browser\ScriptBrowser.dll' -ErrorAction Stop
	Add-Type -Path 'C:\Program Files (x86)\Microsoft Corporation\Microsoft Script Browser\BestPractices.dll' -ErrorAction Stop
	$scriptBrowser = $psISE.CurrentPowerShellTab.VerticalAddOnTools.Add('Script Browser', [ScriptExplorer.Views.MainView], $true)
	$scriptAnalyzer = $psISE.CurrentPowerShellTab.VerticalAddOnTools.Add('Script Analyzer', [BestPractices.Views.BestPracticesView], $true)
	$psISE.CurrentPowerShellTab.VisibleVerticalAddOnTools.SelectedAddOnTool = $scriptBrowser
#Script Browser End
} catch {
	Write-Warning -Message "Failed because $($_.Exception.message)"
}

To fix the error, I installed Microsoft .NET Framework 4.5.1 (Offline Installer) that

is a highly compatible, in-place update to the Microsoft .NET Framework 4 and the Microsoft .NET Framework 4.5.

Then I read a second time the page and complied with the ‘install instruction’

  1. Start Windows PowerShell with the “Run as administrator” option.
  2. Run this command: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned

but I run into a second issue:
As you can see on the following screenshot, there’s a message saying Network error, please check your internet connection and proxy settings in the newly loaded ‘script browser’ tab.

Worse, the Update-Help and Invoke-WebRequest don’t run anymore although a procmon trace shows that the HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\ProxySettingsPerUser isn’t found and that my proxy settings are being read and used correctly from the Current User registry hive.
I used the following code snippet I posted on this blog post when investigating another proxy issue in order to determine what proxy settings the ISE was reading from the HKCU hive.

-join (
(Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections').DefaultConnectionSettings |
Foreach { [char][int]"$_"}
)            

Then I removed the newly created Microsoft.PowerShellISE_profile.ps1 responsible for displaying the script browser tab and confirmed that Update-Help and Invoke-WebRequest work without any issue.

Then I loaded manually the script browser and again it had the same issue.

After that I clicked on the option symbol I’ve highlighted

…and as soon as I’ve set the ‘use Internet Explorer’ settings, everything worked πŸ™‚

Bonus:
You can view the settings with:

[ScriptExplorer.Properties.Settings]::Default

That said. You must also be warned. Don’t try to set properties on ScriptExplorer.Properties.Settings default instance although it appears to be possible.
You may end up with inconsistent settings and the script explorer may not launch anymore.

If you mess with the settings of the ScriptExplorer.Properties.Settings default instance, you might need to delete the user.config while the ISE is running as it gets recreated when the ISE is closed.

Get-ChildItem "$env:userprofile\AppData\Local\Microsoft_Corporation\powershell_ise*" -Include user.config -Recurse -Force -ErrorAction SilentlyContinue | 
Remove-Item -Verbose

The settings of the script explorer are stored in the user.config file and can be also viewed like this:

$xml = ([xml](Get-Content (Get-ChildItem "$env:userprofile\AppData\Local\Microsoft_Corporation\powershell_ise*" -Include user.config -Recurse -Force -ErrorAction SilentlyContinue)))
$xml.configuration.userSettings."ScriptExplorer.Properties.Settings".setting

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

*! Update: 2014-09-30:
Looking for the latest version of ADK 8.1, see my most recent post

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

I proposed last year two scripts to download the latest Windows Assessment and Deployment Kit (ADK) files using BITS and PowerShell πŸ™‚

Today is April 8, 2014 and it’s the perfect time to update the script.

But let’s first see how I built the DATA blocks in the function.

I first launched the webinstaller of the ADK and told him to download files into C:\ADK.
Then I used PowerShell to extract the DATA using the following code.

# First test the regular expression with the built-in select-string cmdlet
Get-Content 'C:\adk\Windows Assessment and Deployment Kit for Windows 8.1_20140403114657.log' | 
Select-String -Pattern "Acquiring\spackage:\s(?<Name1>.*),\spayload:\s(?<Name2>.*),\sdownload\sfrom:\s(?<URL>.*)$"

# Extract URL and file names from the log file and send it through the pipeline
$ADKFiles = (
Get-Content "C:\adk\Windows Assessment and Deployment Kit for Windows 8.1_20140403114657.log" -ReadCount 1 | ForEach-Object {
    ([regex]'Acquiring\spackage:\s(?<Name1>.*),\spayload:\s(?<Name2>.*),\sdownload\sfrom:\s(?<URL>.*)$').Matches($_) | ForEach-Object {
        [PSCustomObject]@{
            URL = @($_.Groups)[-1].Value ;
            FileName = ([URI]@($_.Groups)[-1].Value).Segments[-1] -replace '%20'," "
        }
    }
}
)

# Let me know how many files are downloaded
Write-Verbose -Message ("There are {0} files in the ADK toolkit to download" -f $Adkfiles.Count) -Verbose

Write-Verbose -Message (
    "There are {0} files in the ADK toolkit from ../adk/Installers" -f ($Adkfiles| Where URL -match "adk/Installers/").Count
) -Verbose

Write-Verbose -Message (
    "There are {0} files in the ADK toolkit from ../adk/Patches" -f ($Adkfiles| Where URL -notmatch "adk/Installers/").Count
) -Verbose

# Construct the first DATA block
$count = 0
($Adkfiles| Where URL -match "adk/Installers/") | ForEach-Object {
 '{0}={1}' -f $count,$_.FileName
    $count++
}
# Construct the second DATA block
$count = 0
($Adkfiles| Where URL -notmatch "adk/Installers/") | ForEach-Object {
 '{0}={1}' -f $count,$_.FileName
    $count++
}


Then I copied the console output you can see above into the appropriate DATA blocks and updated manually the version string from 8.100.26020 to 8.100.26629

#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
    }
}
  
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=Toolkit Documentation-x86_en-us.msi
                1=e5f4f4dc519b35948be4500a7dfeab14.cab
                2=56e5d88e2c299be31ce4fc4a604cede4.cab
                3=4fc82a5cedaab58e43b487c17f6ef6f3.cab
                4=d562ae79e25b943d03fc6aa7a65f9b81.cab
                5=dotNetFx45_Full_x86_x64.exe
                6=Application Compatibility Toolkit-x86_en-us.msi
                7=3d610ba2a5a333717eea5f9db277718c.cab
                8=0a3a39d2f8a258e1dea4e76da0ec31b8.cab
                9=f7699e5a82dcf6476e5ed2d8a3507ace.cab
                10=e65f08c56c86f4e6d7e9358fa99c4c97.cab
                11=Microsoft Compatibility Monitor-x86_en-us.msi
                12=Application Compatibility Toolkit-x64_en-us.msi
                13=83bd1072721871ea0bdc4fab780d9382.cab
                14=Microsoft Compatibility Monitor-x86_en-us.msi
                15=Windows Deployment Tools-x86_en-us.msi
                16=3b71855dfae6a44ab353293c119908b8.cab
                17=a011a13d3157dae2dbdaa7090daa6acb.cab
                18=b6758178d78e2a03e1d692660ec642bd.cab
                19=4defb086385752d8cd0d1432900fb4ca.cab
                20=630e2d20d5f2abcc3403b1d7783db037.cab
                21=a7eb3390a15bcd2c80a978c75f2dcc4f.cab
                22=56dd07dea070851064af5d29cadfac56.cab
                23=870d7f92116bc55f7f72e7a9f5d5d6e1.cab
                24=c98a0a5b63e591b7568b5f66d64dc335.cab
                25=eacac0698d5fa03569c86b25f90113b5.cab
                26=f2a850bce4500b85f37a8aaa71cbb674.cab
                27=8624feeaa6661d6216b5f27da0e30f65.cab
                28=388dee738d7d1c99d6fe776a85ee32f8.cab
                29=413a073d16688e177d7536cd2a64eb43.cab
                30=500e0afd7cc09e1e1d6daca01bc67430.cab
                31=9050f238beb90c3f2db4a387654fec4b.cab
                32=5d984200acbde182fd99cbfbe9bad133.cab
                33=abbeaf25720d61b6b6339ada72bdd038.cab
                34=1439dbcbd472f531c37a149237b300fc.cab
                35=2517aec0259281507bfb693d7d136f30.cab
                36=d2611745022d67cf9a7703eb131ca487.cab
                37=f480ed0b7d2f1676b4c1d5fc82dd7420.cab
                38=0c48c56ca00155f992c30167beb8f23d.cab
                39=662ea66cc7061f8b841891eae8e3a67c.cab
                40=36e3c2de16bbebad20daec133c22acb1.cab
                41=fcc051e0d61320c78cac9fe4ad56a2a2.cab
                42=69f8595b00cf4081c2ecc89420610cbd.cab
                43=ea9c0c38594fd7df374ddfc620f4a1fd.cab
                44=bbf55224a0290f00676ddc410f004498.cab
                45=Windows System Image Manager on amd64-x86_en-us.msi
                46=Windows System Image Manager on x86-x86_en-us.msi
                47=Windows Deployment Customizations-x86_en-us.msi
                48=93ed81ef8cf2e77c6ebc8aba5d95b9cf.cab
                49=377a2b6b26ea305c924c25cf942400d6.cab
                50=4e56c6c11e546d4265da4e9ff7686b67.cab
                51=WimMountAdkSetupX86.exe
                52=WimMountAdkSetupAmd64.exe
                53=WimMountAdkSetupArm.exe
                54=InstallRegHiveRecoveryDriverX86.exe
                55=InstallRegHiveRecoveryDriverAmd64.exe
                56=Windows PE x86 x64-x86_en-us.msi
                57=a32918368eba6a062aaaaf73e3618131.cab
                58=a32918368eba6a062aaaaf73e3618131.cab
                59=a32918368eba6a062aaaaf73e3618131.cab
                60=a32918368eba6a062aaaaf73e3618131.cab
                61=0b63b7c537782729483bff2d64a620fa.cab
                62=9722214af0ab8aa9dffb6cfdafd937b7.cab
                63=aa25d18a5fcce134b0b89fb003ec99ff.cab
                64=Windows PE x86 x64 wims-x86_en-us.msi
                65=690b8ac88bc08254d351654d56805aea.cab
                66=6d3c63e785ac9ac618ae3f1416062098.cab
                67=User State Migration Tool-x86_en-us.msi
                68=5ac1863798809c64e85c2535a27a3da6.cab
                69=4d2878f43060bacefdd6379f2dae89b0.cab
                70=Volume Activation Management Tool-x86_en-us.msi
                71=fe43ba83b8d1e88cc4f4bfeac0850c6c.cab
                72=WPTx86-x86_en-us.msi
                73=WPTx64-x86_en-us.msi
                74=WPT Redistributables-x86_en-us.msi
                75=WPTx86-x86_en-us.msi
                76=WPTx64-x86_en-us.msi
                77=WPTarm-arm_en-us.msi
                78=Windows Assessment Toolkit-x86_en-us.msi
                79=23ca402f61cda3f672b3081da79dab63.cab
                80=0ce2876e9da7f82aac8755701aecfa64.cab
                81=24b9e5f1f97c2f05aa95ee1f671fd3cc.cab
                82=Windows Assessment Toolkit (AMD64 Architecture Specific)-x86_en-us.msi
                83=Windows Assessment Toolkit (X86 Architecture Specific)-x86_en-us.msi
                84=Assessments on Client-x86_en-us.msi
                85=4d15138ec839ce36f5b68c16b332920a.cab
                86=cd23bfdfd9e3dfa8475bf59c2c5d6901.cab
                87=86ae476dfe0498a5b5d1b6f3076412c7.cab
                88=6da2af86cb1227e66cf9bc85f2786782.cab
                89=a565f18707816c0d052281154b768ac0.cab
                90=b0189bdfbad208b3ac765f88f21a89df.cab
                91=be7ebc1ac434ead4ab1cf36e3921b70e.cab
                92=781e7c95c1b6b277057c9b53b7b5a044.cab
                93=036c618de505eeb40cca35afad6264f5.cab
                94=450f8c76ee138b1d53befd91b735652b.cab
                95=3eaef6a740a72a55f4a0ac3039d05419.cab
                96=1620efa4ffe2a6563530bd0158b17fe6.cab
                97=125b1c8c81e36ec9dbe5abf370ff9919.cab
                98=fa7c072a4c8f9cf0f901146213ebbce7.cab
                99=94cae441bc5628e21814208a973bbb9d.cab
                100=f8f7800500b180b8a2103c40ce94f56a.cab
                101=b5227bb68c3d4641d71b769e3ac606a1.cab
                102=1f90b0f7321fab8dcdedaba3b30415f3.cab
                103=d519967dbb262c80060d9efb5079aa23.cab
                104=a30d7a714f70ca6aa1a76302010d7914.cab
                105=b3892d561b571a5b8c81d33fbe2d6d24.cab
                106=aa4db181ead2227e76a3d291da71a672.cab
                107=8c27542f7954c25af62730fbb1e211d2.cab
                108=7ab29d7f105f1e7814198f23b60f8e5d.cab
                109=0d981f062236baed075df3f42b1747db.cab
                110=a03686381bcfa98a14e9c579f7784def.cab
                111=3585b51691616d290315769bec85eb6f.cab
                112=fd5778f772c39c09c3dd8cd99e7f0543.cab
                113=57007192b3b38fcd019eb88b021e21cc.cab
                114=c6babfeb2e1e6f814e70cacb52a0f923.cab
                115=fbcf182748fd71a49becc8bb8d87ba92.cab
                116=17c9d60f2bc5bc54c58782d614afcbf0.cab
                117=3dc1ed76e5648b575ed559e37a1052f0.cab
                118=6d2cfb2c5343c33c8d9e54e7d1f613f9.cab
                119=625aa8d1c0d2b6e8cf41c50b53868ecd.cab
                120=45c632fb53b95fe3bd58a6242325afa6.cab
                121=d5abe4833b23e13dc7038bde9c525069.cab
                122=ac9ff098e23012b74624db792b538132.cab
                123=cfb8342932e6752026b63046a8d93845.cab
                124=bd748d6fbff59b2a58cebdb99c3c6747.cab
                125=38d93b8047d5efb04cf01ab7ec66d090.cab
                126=39837d43d71c401e7edc9ba3e569cd69.cab
                127=0708be5ffbe332f6a1571c929c1322a5.cab
                128=3814eaa1d4e897c02ac4ca93e7e7796a.cab
                129=7011bf2f8f7f2df2fdd2ed7c82053d7f.cab
                130=ab3291752bc7a02f158066789e9b0c03.cab
                131=c0f42c479da796da513cc5592f0759d3.cab
                132=3611bd81544efa3deb061718f15aee0c.cab
                133=0302dc615b0a5fd4810430b2cdacb5e3.cab
                134=6dc62760f8235e462db8f91f6eaa1d90.cab
                135=268b1a41f6bd2906449944b964bf7393.cab
                136=527b957c06e68ebb115b41004f8e3ad0.cab
                137=a1d26d38d4197f7873a8da3a26fc351c.cab
                138=6bdcd388323175da70d836a25654aa92.cab
                139=11bdc4a4637c4d7ab86107fd13dcb9c6.cab
                140=7c11b295fb7f25c6d684b1957e96a226.cab
                141=77adc85e5c49bbd36a91bb751dc55b39.cab
                142=732eefaf52275b7a708311a31c82c814.cab
                143=ed711e0a0102f1716cc073671804eb4c.cab
                144=bc1fef9daa903321722c08ce3cf51261.cab
                145=SQLEXPR_x86_ENU.exe
                146=Windows Assessment Services-x86_en-us.msi
                147=7c195d91008a0a6ad16e535ac228467d.cab
                148=eebe1a56de59fd5a57e26205ff825f33.cab
                149=6894c1e1e549c4ab533078e3ff2e92af.cab
                150=Windows Assessment Services - Client (Server SKU)-x86_en-us.msi
                151=5775a15b7f297f3e705a74609cb21bbc.cab
                152=18e5e442fc73caa309725c0a69394a46.cab
                153=Windows Assessment Services - Client (AMD64 Architecture Specific, Server SKU)-x86_en-us.msi
                154=Assessments on Server-x86_en-us.msi
                155=wasinstaller.exe
                156=Windows Assessment Services - Client (Client SKU)-x86_en-us.msi
                157=Windows Assessment Services - Client (X86 Architecture Specific, Client SKU)-x86_en-us.msi
                158=Windows Assessment Services - Client (AMD64 Architecture Specific, Client SKU)-x86_en-us.msi
                159=Kits Configuration Installer-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\8.100.26629" | 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/8.100.26629/$($PatchesURLs[$i.ToString()])"
            $Destination = Join-Path -Path (Join-Path -Path $TargetFolder -ChildPath "Patches/8.100.26629") -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 {}
}

Then I tested the function like this:

  1. I created the target folder
  2. If you don’t create it and try to launch the function, you’ll get the following warning:

  3. I dotsourced the script
  4. I launched the download


  5. I compared the downloaded files made with the webinstaller and my function.

Bonus:
The following links are still valid to obtain the:

Analysing files with virustotal.com public API

I’ve sometimes some forensics tasks to do at work. A member of the network team gave me a USB stick with the incoming mail server traffic.
My task was to get files’ thumbprints and query virustotal.com using its public API to see if these file hashes are known. I’d have used the private API of virustotal if I’d the right to upload the file to virustotal.com.

Before plugging-in the USB stick, I uninstalled my antivirus and then disabled Windows Defender.

I recycled some obsolete code I already published in January 2013 that you can find on this page.

One of the problems with my previous code was that the URL https://www.virustotal.com/search/ it used is not valid anymore. Now virustotal supports multilanguage and the correct URL is https://www.virustotal.com/en/search/

The second problem with my code was that the core regular expression is obsolete and doesn’t match the updated HTML code returned by a search.

To fix the code, I had to shift back to the methodology I used in the first place in 2013 before coming up with the PowerShell code.
In other words, I had to analyse the web queries (hearders and redirection) and their HTML code.
I submitted manually both a known safe SHA2 thumbprint of a file, a24f400c4fc6b7d4085f9e264f6d3f70c13d678c0e34b3e31f9163cf10e423ec , a known malware thumbprint 7e8bb57ad97ace3aa4a8f3ecaf5538e84b58a06e16569f3f60f190fa3e83f80b and a totally unknown thumbprint that has never been submitted to virustotal.com
…and started a network capture in IE11

Then with the above SHA2 checksum, I did:

$res = (Invoke-WebRequest -Uri 'https://www.virustotal.com/en/search/' -Method Post -Body "query=$hash" -MaximumRedirection 0 -ErrorAction SilentlyContinue)            
$page = Invoke-WebRequest -Method GET -Uri $res.Headers.Location -ErrorAction SilentlyContinue -MaximumRedirection 0

and I started to use the excellent Get-Matches function provided by Dr. Tobias Weltner: http://powershell.com/cs/blogs/tobias/archive/2011/10/27/regular-expressions-are-your-friend-part-1.aspx

I started to find a regular expression that matches each case:

$page.AllElements.FindById('antivirus-results').outerHTML | 
Get-Matches -Pattern '<TD\sclass=ltr>(?<ID>.*)\s</TD>'

$page.AllElements.FindById('antivirus-results').outerHTML | 
Get-Matches -Pattern '<TD\sclass="ltr\stext\-green"><I\stitle="(?<ID>.*)"\sclass=icon\-ok\-sign\sdata\-toggle="tooltip"></I></TD>'

for the following HTML code:
and

$page.AllElements.FindById('antivirus-results').outerHTML | 
Get-Matches -Pattern '<TD\sclass="ltr\stext-red">(?<ID>.*)\s</TD>'

for the following HTML code:

The last step consisted in merging all the 3 regular expressions into a single one like this:

$page.AllElements.FindById('antivirus-results').outerHTML | 
Get-Matches -Pattern '<TD\sclass=("?)ltr(>|\stext\-green"><I\stitle="|\stext-red">)(?<ID>.*)("\sclass=icon\-ok\-sign\sdata\-toggle="tooltip"></I></TD>|\s</TD>)'

Here’s the full code I used to obtain at the end an Excel file containing the results:

#Requires -version 4.0

$allfiles = @()
# We first clear all errors in the automatic variable
$Error.Clear()
# We capture all files and let error happen silently and being logged into the $error automatic variable
$allfiles = Get-ChildItem -Path C:\ZIP -Recurse -Force -Include * -File -ErrorAction SilentlyContinue

# Let us know what happen
$Error | Where { $_.CategoryInfo.Reason -eq "PathTooLongException" } | ForEach-Object -Begin{
    Write-Warning -Message "The following folders contain a file longer than 260 characters"
    # Get-ChildItem : The specified path, file name, or both are too long. 
    # The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.
} -Process {
    $_.CategoryInfo.TargetName
}
Write-Verbose -Message ('There {0} other type of errors' -f ($Error | Where { $_.CategoryInfo.Reason -ne "PathTooLongException" }).Count) -Verbose
Write-Verbose -Message ("There's a total of {0} files" -f $allfiles.Count) -Verbose

# Show extensions by occurence
$allfiles | Group -NoElement -Property Extension | Sort -Property Count -Descending

Start-Sleep -Seconds 1

$totalzip = ($allfiles | Where { $_.Extension -eq '.zip' }).Count
$filecount = 0

# Select only files with a zip extension
$results = @()
$allfiles | Where { $_.Extension -eq '.zip' } | 
ForEach-Object {
   
    $filecount++

    $hash = $res = $page = $checksum = $obj = $outtext = $null
    
    Start-Sleep -Milliseconds (Get-Random -Maximum 750 -Minimum 500)

    $hash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash
    
    Write-Verbose -Message ('Searching file {2}/{3} on {0} with sha256 {1}' -f $_.FullName,$hash,$filecount,$totalzip) -Verbose
    
    # Append a SHA256
    $_ | Add-Member -MemberType NoteProperty -Name SHA256 -Value $hash -Force

    # Search virustotal by SHA256
    $res = (Invoke-WebRequest -Uri 'https://www.virustotal.com/en/search/' -Method Post -Body "query=$hash" -MaximumRedirection 0 -ErrorAction SilentlyContinue)

    if ($res.StatusCode -eq 302 ) {
        if ($res.Headers.Location) {
            try {
                $page = Invoke-WebRequest -Method GET -Uri $res.Headers.Location -ErrorAction SilentlyContinue -MaximumRedirection 0
            } catch {
                Write-Warning -Message "The request on $($res.Headers.Location) returned $($_.Exception.Message)"
            }
            if ($page.Headers.Location -notmatch "file/not/found/") {
                try {
                    $obj = New-Object -TypeName PSObject
                    $outtext = ($page.AllElements | Where { ($_.TagName -eq 'TBODY') -and ($_.outerHTML -match "$hash") -and ($_.outerText -match "Detection\sratio") }).OuterText
                    if ($outtext) {
                        $outtext -split "`n" |  ForEach-Object {
                            if ($_ -match ":") {
                            $obj | Add-Member -MemberType NoteProperty -Name ($_ -split ":")[0] -Value (-join($_ -split ":" )[1..($_ -split ":" ).count]) -Force
                            } else {
                                Write-Warning -Message "Mismatch with ':'"
                                $_
                            }
                        }
                        # Analysis tab
                        $count = 0
                        $analysisar = @()
                        $AVName = $DetectionDate = $DetectionRate = $null
   
                        ([regex]'<TD\sclass=("?)ltr(>|\stext\-green"><I\stitle="|\stext-red">)(?<ID>.*)("\sclass=icon\-ok\-sign\sdata\-toggle="tooltip"></I></TD>|\s</TD>)').Matches(
                        $page.AllElements.FindById('antivirus-results').outerHTML) | ForEach-Object -Process {
                            $count++
                            switch ((@($_.Groups))[-1]) {
                                {$_ -match '\d{8}'} { $DetectionDate = $_ ; break}
                                {$_ -match '(^\-$|^File\snot\sdetected$)'}{ $DetectionRate = '-' ; break }
                                {$_ -match '.*'} {
                                    if ($count -eq 1) {
                                        $AVName = $_
                                    } else {
                                        $DetectionRate = $_
                                    }
                                    ; break
                                }
                            }
                            if ($count -eq 3) {
                                $count = 0
                                $analysisar += New-Object -TypeName PSObject -Property @{
                                    Update = $DetectionDate
                                    Result = $DetectionRate
                                    Antivirus = $AVName
                                }
                            }
                        }
                        $obj | Add-Member -MemberType NoteProperty -Name Analysis -Value $analysisar -Force
                        $obj
                        $_ | Add-Member -MemberType NoteProperty -Name VTResults -Value $obj -Force
                    } else {
                        # Write-Warning -Message "$outtext # is because the file has probably been never submitted"
                        # it shouldn't happen but... who knows
                        $_ | Add-Member -MemberType NoteProperty -Name VTResults -Value ([string]::Empty) -Force
                    }
                } catch {
                    $_
                }
            } else {
                Write-Warning -Message "the file was not found"
                $_ | Add-Member -MemberType NoteProperty -Name VTResults -Value "Unknown by VT" -Force
            }
        } else {
            Write-Warning -Message "the location in the header is empty"
            $_ | Add-Member -MemberType NoteProperty -Name VTResults -Value "Header empty issue" -Force
        }
    } else {
        Write-Warning -Message "the page returned a $($res.StatusCode) status code"
        $_ | Add-Member -MemberType NoteProperty -Name VTResults -Value "Status code issue" -Force
    }
    $results += $_
}

Write-Verbose -Message ("There's a total of {0} results" -f $results.Count) -Verbose

Write-Verbose -Message ("There's a total of {0} unknown files" -f (
    $results | Where 'VTResults' -eq "Unknown by VT").Count
) -Verbose

# Export results to CSV

# First unknown files
($results | Where 'VTResults' -eq "Unknown by VT") | 
Select Name,FullName,SHA256,
    @{l='Ratio';e={'Unknown by VT'}},
    @{l='MalwareName';e={[string]::Empty}} | 
Export-Csv -Path "$($env:USERPROFILE)\Documents\VT.Zipfiles.analysis.csv" -Encoding UTF8  -NoTypeInformation -Delimiter ";"

# Then export files that are identified as malware
Write-Verbose -Message ("There's a total of {0} known files" -f (
    $results | Where 'VTResults' -notin @("Unknown by VT","Header empty issue","Status code issue")).Count
) -Verbose

($results | Where 'VTResults' -notin @("Unknown by VT","Header empty issue","Status code issue")) |
Select Name,FullName,SHA256,
    @{l='Ratio';e={
        $_.VTResults.'Detection Ratio' -replace '\s',''
    }},
    @{l='MalwareName';e={
        ($_.VTResults.Analysis | Where { $_.Result -notmatch "^\-$"}).Result -as [string[]]
    }} |
Export-Csv -Path "$($env:USERPROFILE)\Documents\VT.Zipfiles.analysis.csv" -Encoding UTF8 -Append -NoTypeInformation -Delimiter ";"

With the above code, I think it took about an hour to complete the scan of ~2500 files (I went to lunch and didn’t measure it).
Notice the Start-Sleep cmdlet in the foreach loop to avoid being too aggressive with the virustotal public API.
Here are some samples of the screen output I had while processing files.



And here is the Excel file after 2 hours of work πŸ˜€

PowerShell rocks! 😎

LogParser vs. PowerShell

The following article popped-up this morning about How to extract NETBIOS name from BADMIF files on ConfigMgr 2012 using Log Parser 2.2

Hey, why would I need the old school log parser from 2005 when we have PowerShell nowadays and then another tool to manipulate the data output?

You can actually achieve this task with a PowerShell one-liner and even without having to manipulate the output.

Get-ChildItem -Path "C:\Program Files\Microsoft Configuration Manager\inboxes\auth\dataldr.box\BADMIFS" -Include *.MIF -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object { 
    try {
        (
            Get-Content -ReadCount 1 -TotalCount 6 -Path $_.FullName -ErrorAction Stop  | 
            Select-String -Pattern "//KeyAttribute<NetBIOS\sName><(?<ComputerName>.*)>" -ErrorAction Stop 
        ).Matches.Groups[-1].Value 
    } catch {
        Write-Warning -Message "Failed" 
    }
}

If you’ve got warnings, you may also want to investigate which files caused it.
In this case, I propose the following modification of the above code to be able to catch these files.

$ConfigMgrBoxPath = "C:\Program Files\Microsoft Configuration Manager\inboxes\auth\dataldr.box\BADMIFS"
Get-ChildItem -Path $ConfigMgrBoxPath -Include *.MIF -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object { 
    $File = $_.FullName ;
    try {
        (
            Get-Content -ReadCount 1 -TotalCount 6 -Path $_.FullName -ErrorAction Stop  | 
            Select-String -Pattern "//KeyAttribute<NetBIOS\sName><(?<ComputerName>.*)>" -ErrorAction Stop 
        ).Matches.Groups[-1].Value 
    } catch {
        Write-Warning -Message "Failed for $File" 
    }
}

PowerShell rocks, no doubt! 😎

Friday fun: Find all local groups and their members

Without my favourite tool (PowerShell) in a pure DOS command prompt, this is how I’d have displayed the groups and their members:

for /f "tokens=1,* delims=*" %i in ('net localgroup ^| findstr /R /C:"^\*" ') do @ (echo.&echo Group:%i&echo. & for /f "tokens=* skip=6" %z in ( 'net localgroup "%i" ^| findstr /v /C:"The command completed successfully."') do @ echo %z)

When mixing the native DOS command and some PowerShell regular expression, this is how I’d get the same result displayed:

(net localgroup) -match "^\*" -replace "\*","" | foreach { 
    "`nGroup:$($_)`n" ; 
    (net localgroup "$_") | Select-String -Pattern "^\-","^The\sCommand","^Alias\sname","^Comment","^Members","^(\S)?$" -notmatch
}

If I had to achieve the same task only in PowerShell without calling native DOS commands, I’d do:

([ADSI]"WinNT://$($env:computername),computer").psbase.children | where { $_.psbase.schemaClassName -eq 'group' } | foreach {
    "`nGroup:$($_.name)`n"
    ([ADSI]$_.psbase.Path).psbase.Invoke("Members") | foreach {
        $_.GetType().InvokeMember("Name",'GetProperty',$null,$_, $null)
    }
}

Note that I’ve adapted the above code from PowerShell MVP Shay Levy that can be found on this page

I’ve modified his code because the following syntax (the children enumeration actually) falls in an infinite loop on my windows 8.1

$server="."
$computer = [ADSI]"WinNT://$server,computer"
$computer.psbase.children

There are other annoyances. If I launch the following as a standard user it works

([ADSI]"WinNT://$($env:userdomain)/$($env:computername)").psbase.children

…but I get the following errors at the end of the enumeration:
format-default : The following exception occurred while retrieving members: “Unknown error (0x80005004)”
+ CategoryInfo : NotSpecified: (:) [format-default], ExtendedTypeSystemException
+ FullyQualifiedErrorId : CatchFromBaseGetMembers,Microsoft.PowerShell.Commands.FormatDefaultCommand

…and I don’t get the above error with Powershell running as Admin.

I wonder why all WinNT ADsPath don’t work in with the [ADSI], alias the System.DirectoryServices.DirectoryEntry .Net class.

Is my computer connected to the Internet

When I wrote my second (draft) article entitled How to view and restore hidden Windows Updates with PowerShell on www.powershellmagazine.com I included the following code in a function to test whether the computer was connected to the Internet before initiating a scan.

    Function Test-ConnectedToInternet {
        [Activator]::CreateInstance(
            [Type]::GetTypeFromCLSID(
                [Guid]'{DCB00C01-570F-4A9B-8D69-199FDBA5723B}'
            )
        ).IsConnectedToInternet
    }

It raised some legitimate questions and of course I learned new things I’d like to share πŸ™‚

The first question was about how the Windows Update Agent is configured. In some locked-down environments, computers/servers might not be connected directly to Internet and the WUA is configured to use an internal WSUS server. The function I proposed works with WSUS but would have failed because of the above test I planned to initially add. It was actually wiser not to test whether the computer is connected to the Internet. In other words, a scan can be performed against WSUS even if the computer hasn’t access to the Internet.

The second question was about the ‘IsConnectedToInternet’ property.
The property I’m testing above is the one attached directly to the Network List Manager object.

The same property also exists for all the network interfaces present on the computer.

$nlm = [Activator]::CreateInstance([Type]::GetTypeFromCLSID([Guid]"{DCB00C01-570F-4A9B-8D69-199FDBA5723B}"))
$nlm.GetNetworkConnections() | ForEach-Object { 
    [PSCustomObject]@{
        NetworkName = ($_.GetNetwork().GetName());
        isConnectedToInternet = $_.isConnectedToInternet;
    }
} | Format-Table -AutoSize 


So the property directly attached to the Network List Manager ( $nlm.isConnectedToInternet ) is somehow a global property set to summarize what was found per interface. If you’ve multiple interfaces and at least one has access to the Internet, then your computer has access to Internet.

Bonus:
On Windows 8.1, I’ve also got a new cmdlet function Get-NetConnectionProfile that will do the job.

Get-NetConnectionProfile -IPv4Connectivity Internet


Behind the scene this new cmdlet function queries and presents more nicely what you can find in the WMI repository by executing

Get-WmiObject -Namespace root/StandardCimv2 -Class MSFT_NetConnectionProfile

What is exactly the IPv4connectivity property returned by the Get-NetConnectionProfile cmdlet?
It’s easy to get its definition like this,

Get-NetConnectionProfile | Select -First 1 |            
Get-Member | Where Name -eq "IPv4Connectivity" |            
Select -ExpandProperty Definition
System.Object IPv4Connectivity {get=[Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetConnectionProfile.IPv4Connectivity]($this.PSBase.CimInstanceProperties['IPv4Connectivity'].Value);set=param($newValue)
          $this.PSBase.CimInstanceProperties['IPv4Connectivity'].Value = [System.UInt32][Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetConnectionProfile.IPv4Connectivity]$newValue;}

One can discover the translation being operated between numeric values and its literal definition like this:

0..4| % {            
'{0}>{1}'-f $_,            
[Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetConnectionProfile.IPv4Connectivity]$_            
}