Task Sequence Powershell Module

Awhile ago I submitted a request to Microsoft for a built-in Powershell Module during the Task Sequence. Mostly for basic things like getting and setting variables, but once I noticed what all the COM Objects exposed, I wanted to take this a step further.

Primarily you’ll have 2 COM objects you can create during a Task Sequence: Microsoft.SMS.TSEnvironment (getting/setting variables) and Microsoft.SMS.TSProgressUI (manipulating the Progress and messages). I’ve uploaded the module for you all to use at my github. I’ll run though some of the details below including visuals for the TSProgressUI items. Some, if not most, of these are-for lack of a more appropriate label-useless; but can still be fun to mess around with!

https://github.com/ndswanson/TaskSequenceModule

Microsoft.SMS.TSEnvironment

Confirm-TSEnvironment

This function is not exported, as it is used within the module to verify that the Microsoft.SMS.TSEnvironment COM Object is loaded before attempting any operations.

Get-TSVariables

Calls “GetVariables()” on the COM Object. This will return an array of all Task Sequence Variable NAMES only.

Get-TSValue

Calls “Value()” on the COM Object to return the value of a specific variable. the “Name” Parameter should be the Task Sequence Variable you want the value for.

Set-TSVariables

Assigns a value to a specific variable. A Boolean return indicates success. Note this is wrapped in a try/catch in the event you attempt to set a read-only variable.

Get-TSAllValues

Returns an object of all Task Sequence Variables and their assigned Values.

Microsoft.SMS.TSProgressUI

Confirm-TSProgressUISetup

This function is not exported, as it is used within the module to verify that the Microsoft.SMS.TSProgressUI COM Object is loaded before attempting any operations.

Show-TSActionProgress

Calls “ShowActionProgress()” on the COM object. This is the function I use 99.9% of the time for UI related actions. This will add the “second” progress bar and update the Text for that progress bar as well as fill the progress meter as you see fit. The neat thing here, is that the “Step” and “MaxStep” will automatically show a “100%” representation. For example, if i have 8 of 400 complete, I don’t have to do the extra scripting to pass in “2” and “100”. I can pass in 8, and 400, and the COM Object does the math for us.

There are other parameters here with “ShowActionProgress()”. With this function, those use the built-in variables “designed” to be used here. If you want to extend this function to allow those additional parameters, feel free. Personally, I don’t find them useful enough to include.

Step Param set to 5, MaxStep Param set to 10.
Show-TSActionProgress

Close-TSProgress

Calls “CloseProgressDialog()” on the COM object. Closes or “hides” the progress bar window. This will automatically reappear once the current step has completed, or if you call one of the “show” functions for the UI.

Show-TSProgress

Calls “ShowTSProgress” on the COM object. This function manipulates the “Top part” of the progress bar. If, for whatever reason, you wanted to manipulate the progress bar without adding the “second/bottom” bar, this is what you should use.

Step Param set to 5, MaxStep Param set to 10.
Show-TSProgress

Show-TSErrorDialog

Calls “ShowErrorDialog()” on the COM object. This wills how the typical “Error in the Task Sequence” Window. There are parameters for the error title, error message/details, exit code, a timeout, and a force reboot. The force reboot will trigger a reboot after the timeout (in seconds) has expired.

Show-TSErrorDialog

Show-TSMessage

Calls “ShowMessage()” on the COM Object. This essentially shows a MessageBox allowing you to specify the button configuration. The interesting (and somewhat useless) part of this, is that you do not receive a return value. If you want a simple “OK” message box, this should work, otherwise I’d recommend using a Normal MessageBox.

Button Options (Type Parameter):

  • 0 = OK
  • 1 = OK, Cancel
  • 2 = Abort, Retry, Ignore
  • 3 = Yes, No, Cancel
  • 4 = Yes, No
  • 5 = Retry, Cancel
  • 6 = Cancel, Try Again, Continue

Type Param set to 0.
Show-TSMessage

Show-TSRebootDialog

Calls “ShowRebootDialog()” on the COM Object. This will allow you to present a custom reboot window. I would recommend against this, as untimely reboots during a task sequence can cause some issues. This is essentially what causes the issue where Updates cause Double Reboots during OSD. If you do need a reboot after a step, either inset a restart computer step in your task sequence or just have your script exit “3010”.

Typically OrganzationName will use the TS Variable “_SMSTSOrgName”.
Show-TSRebootDialog

Show-TSSwapMediaDialog

Calls “ShowSwapMediaDialog()” on the COM Object. If you’ve ever used standalone media that was broken up into multiple CD/DVD sets (or played a Final Fantasy on Playstation), you’ve seen this one. This is a message indicating for you to swap media, letting you know what disk number to insert.

Show-TSSwapMediaDialog

 

Enjoi!

Configuration Manager: Content Location Requests

Not that long ago I was challenged with the task of writing a piece of automation for a ConfigMgr Client to check if a specific package was available in it’s boundary; or commonly referred to as a “Content Location Request”. All I was able to find was Robert Marshall’s [MVP] postings for Content Location Request (CLR) Generator (and the Console Edition). The hints at the ConfigMgr SDK we’re clear, so I went ahead and installed it.

To get a jump start I began looking for examples and yet again found a post by Robert Marshall.

The best part here is this works with SCCM 2012 AND the latest Technical Preview that Microsoft has released for Configuration Manager vNext!

So, here we go! Content Location Request’s using C# or Powershell!

With the ConfigMgr SDK installed we’ll be focusing on the Client Messaging SDK: C:\Program Files (x86)\Microsoft System Center 2012 R2 Configuration Manager SDK\Samples\Client Messaging SDK. Inside this folder you’ll see a Sample.cs file and help file (most important!). if you decide to go beyond what I do here, you’ll want to keep the help file close by! Navigating up and in the Redistributables folder you’ll find the Microsoft.ConfigurationManagement.Messaging.dll.

Loading Files

If using Visual Studio, add a reference to the Messaging dll and the appropriate using reference. While you’re at it add a using statement for System.Diagnostics if one does not already exist.

C#

using System.Diagnostics;
using Microsoft.ConfigurationManagement.Messaging.Framework;
using Microsoft.ConfigurationManagement.Messaging.Messages;
using Microsoft.ConfigurationManagement.Messaging.Sender.Http;

Powershell

[void] [Reflection.Assembly]::LoadWithPartialName("System.Diagnostics")
[void] [Reflection.Assembly]::LoadFile("c:\Microsoft.ConfigurationManagement.Messaging.dll")

When using the “LoadFile” method, you’ll need to put in the full path (no variables). In most cases this is an issue, especially if including the dll within the script path. Best way I’ve found is to us the Add-Type cmdlet (where a variable populated path is allowed) in a try block, then have a catch statement to copy the file and then call LoadFile. If using this in WinPE, you’ll NEED to do this due to the loadFromRemoteSouces default setting.

Powershell if anticipating WinPE:

$Script:CurrentDirectory = Split-Path -Parent $MyInvocation.MyCommand.Definition

try
{
   Add-Type -Path "$Script:CurrentDirectory\Utilities\Microsoft.ConfigurationManagement.Messaging.dll" | out-null
}
catch
{
   Copy-Item -Path "$Script:CurrentDirectory\Utilities\Microsoft.ConfigurationManagement.Messaging.dll" -Destination "x:\Microsoft.ConfigurationManagement.Messaging.dll" | Out-Null
   [void] [Reflection.Assembly]::LoadFile("x:\Microsoft.ConfigurationManagement.Messaging.dll") | out-null
}
[Reflection.Assembly]::LoadWithPartialName("System.Diagnostics")

Setting up Objects

Now that we have the necessary references and using statements we’ll need to create the objects used through the request:

C#:

HttpSender hSender = new HttpSender();

ConfigMgrContentLocationRequest clr = new ConfigMgrContentLocationRequest();

ConfigMgrContentLocationReply cmReply = new ConfigMgrContentLocationReply();

Powershell:

$httpSender = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Sender.Http.HttpSender

$clr = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ConfigMgrContentLocationRequest

$cmReply = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationReply

Setting Up the Request

Now with our objects setup, we can begin formulating the request. The first thing we’ll need to do is run the “Discover” method of the ConfigMgrContentLocationRequest object. Essentially this grabs the systems IP Addresses, Current AD Site, AD Domain and AD Forest.

Nothing too difficult right? Well, not exactly! The AD Domain and AD Forest discovery uses the ADSI COM object. Still no problem though, right? Well, not if you intend to use this within WinPE. We’ve got 2 options then: (1) Add the ADSI Plugin to our WinPE image (Thanks to Johan Arwidmark), or (2) build a try catch.

Since the ADSI plugin is not included with WinPE and not supported by Microsoft, we’ll just use a simple try/catch. However, there’s another catch, we’ll need to specify the domain and forest strings.

C#:

private string myDomain = "mydomain.com";
private string myForest = "myForest.com";

try
{
   clr.Discover();
}
catch
{
clr.LocationRequest.ContentLocationInfo.IPAddresses.DiscoverIPAddresses();
   clr.LocationRequest.ContentLocationInfo.ADSite.DiscoverADSite();
   clr.LocationRequest.Domain = new ContentLocationDomain() { Name = myDomain };
   clr.LocationRequest.Forest = new ContentLocationForest() { Name = myForest };

}

Powershell:

[string]$myDomain = "mydomain.com"
[string]$myForest = "myforest.com"

try
{
   $clr.Discover()
}
catch
{
   $clr.LocationRequest.ContentLocationInfo.IPAddresses.DiscoverIPAddresses()           $clr.LocationRequest.ContentLocationInfo.ADSite.DiscoverADSite()
   $clr.LocationRequest.Domain = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationDomain
   $clr.LocationRequest.Domain.Name = $myDomain
   $clr.LocationRequest.Forest = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationForest
   $clr.LocationRequest.Forest.Name = $myForest
}

Next we’ll need to setup the parameters for the request. For this we’ll need a Management Point fqdn, Management Point Site Code, Package ID, and Package Source Version.

C#:

clr.SiteCode = mySiteCode;
clr.Settings.HostName = myManagementPoint;
clr.LocationRequest.Package.PackageId = packageID;
clr.LocationRequest.Package.Version = packageVersion;

Powershell:

$clr.SiteCode = $ManagementPointSiteCode
$clr.Settings.HostName = $ManagementPoint
$clr.LocationRequest.Package.PackageId = $PackageID
$clr.LocationRequest.Package.Version = $PackageVersion

Now, perhaps one of my favorite things here, is that we can manually set the AD Site code and get a location request as if we were at a different location. This is especially nice if we don’t have access to systems in those specific AD Sites and want to test!

C#:

if (myCustomAdSite != null)
{
   clr.LocationRequest.ContentLocationInfo.ADSite = myCustomAdSite;
}

Powershell:

if ($myCustomAdSite -ne $null)
{
   $clr.LocationRequest.ContentLocationInfo.ADSite = $myCustomAdSite
}

Next we’ll perform a validate on the message (ConfigMgrContentLocationRequest). This call validates that the message we have setup is good. If Validate fails, when the message is sent, it will undoubtedly fail as well. After validate we’ll send the request.

C#:

clr.Validate((IMessageSender)hSender);
cmReply = clr.SendMessage(hSender);

Powershell:

$clr.Validate([Microsoft.ConfigurationManagement.Messaging.Framework.IMessageSender]$httpSender)
$cmReply = $clr.SendMessage($httpSender)

Now we have our returned result! It will be returned as a string but in XML format. We can easily cast this into an XML object and do whatever we wish! Before returning the value, I’ve had some issues casting this due to a trailing  ” ” (Space) at the end of the string. Below is a quick loop that’ll take care of that, should it exist.

C#:

string response = cmReply.Body.Payload.ToString();

while (response[response.Length - 1] != '>')
{
   response = response.TrimEnd(response[response.Length - 1]);
}

Console.WriteLine(response);

Powershell:

$response = $cmReply.Body.Payload.ToString()

while($response[$response.Length -1] -ne '>')
{
   $response = $response.TrimEnd($response[$response.Length -1])
}

 Write-Output $response

Now we’ve got a nice neat Content Location Request! As you see below there’s quite a bit of information here. My primary accomplishment was checking that “SiteLocality” was “LOCAL” and there were LocationRecords.

Config Mgr 2012 R2 SP1:

<ContentLocationReply SchemaVersion="1.00">
   <ContentInfo PackageFlags="1073741825">
      <ContentHashValues/>
   </ContentInfo>
   <Sites>
      <Site>
         <MPSite IISSSLPreferedPort="443" IISPreferedPort="80" SiteLocality="LOCAL" MasterSiteCode="P01" SiteCode="P01"/>
         <LocationRecords>
            <LocationRecord>
               <ADSite Name="MyADSite"/>
               <IPSubnets>
                  <IPSubnet Address="xxx.xxx.xxx.xxx"/>
                  <IPSubnet Address=""/>
               </IPSubnets>
               <Metric Value=""/>
               <Version>8239</Version>
               <Capabilities SchemaVersion="1.0">
                  <Property Name="SSLState" Value="0"/>
               </Capabilities>
               <ServerRemoteName>myServer.Domain.com</ServerRemoteName>
               <DPType>SERVER</DPType>
               <Windows Trust="0"/>
               <Locality>LOCAL</Locality>
            </LocationRecord>
         </LocationRecords>
      </Site>
   </Sites>
   <RelatedContentIDs/>
</ContentLocationReply>

Config Mgr Technical Preview 4 (v1511):

<ContentLocationReply SchemaVersion="1.00">
   <BoundaryGroups BoundaryGroupListRetrieveTime="2015-12-06T22:19:36.400">
      <BoundaryGroup GroupID="16777217"/>
   <BoundaryGroups/>
   <ContentInfo PackageFlags="16777216">
      <ContentHashValues/>
   </ContentInfo>
   <Sites>
      <Site>
         <MPSite IISSSLPreferedPort="443" IISPreferedPort="80" SiteLocality="LOCAL" MasterSiteCode="P01" SiteCode="P01"/>
         <LocationRecords>
            <LocationRecord>
               <URL Signature="http://myServer.Domain.com/SMS_DP_SMSSIG$/XXX#####" Name="http://myServer.Domain.com/SMS_DP_SMSPKG$/XXX#####"/>
               <ADSite Name="MyADSite"/>
               <IPSubnets>
                  <IPSubnet Address="xxx.xxx.xxx.xxx"/>
                  <IPSubnet Address=""/>
               </IPSubnets>
               <Metric Value=""/>
               <Version>8325</Version>
               <Capabilities SchemaVersion="1.0">
                  <Property Name="SSLState" Value="0"/>
               </Capabilities>
               <ServerRemoteName>myServer.Domain.com</ServerRemoteName>
               <DPType>SERVER</DPType>
               <Windows Trust="0"/>
               <Locality>LOCAL</Locality>
            </LocationRecord>
         </LocationRecords>
      </Site>
   </Sites>
   <RelatedContentIDs/>
</ContentLocationReply>

These seem to differ ever so slightly from 2012 R2 (SP1) to v1511. There’s an added “BoundaryGroups” element and “URL Signature” Element as well. However, nothing that would (at least right now) prevent this code from running and working!

 

Full Code

C#:

// Set up the objects
HttpSender hSender = new HttpSender();

ConfigMgrContentLocationRequest clr = new ConfigMgrContentLocationRequest();

ConfigMgrContentLocationReply cmReply = new ConfigMgrContentLocationReply();

// Discover (the try/catch is in case the ADSI COM
// object cannot be loaded (WinPE).
try
{
 clr.Discover();
}
catch
{
   clr.LocationRequest.ContentLocationInfo.IPAddresses.DiscoverIPAddresses();
   clr.LocationRequest.ContentLocationInfo.ADSite.DiscoverADSite();
   clr.LocationRequest.Domain = new ContentLocationDomain() { Name = myDomain };
   clr.LocationRequest.Forest = new ContentLocationForest() { Name = myForest };
}

// Define our SCCM Settings for the message
clr.SiteCode = mySiteCode;
clr.Settings.HostName = myManagementPoint;
clr.LocationRequest.Package.PackageId = packageID;
clr.LocationRequest.Package.Version = packageVersion;

// If we want to "spoof" a request from a different
// AD Site, we can just pass that AD Site in here.
if (myCustomAdSite != null)
{
   clr.LocationRequest.ContentLocationInfo.ADSite = myCustomAdSite;
}

// Validate clr.Validate((IMessageSender)hSender); // Send the message cmReply = clr.SendMessage(hSender);

// Get response
string response = cmReply.Body.Payload.ToString();

while (response[response.Length - 1] != '>')
{
   response = response.TrimEnd(response[response.Length - 1]);
}

Console.WriteLine(response);

Powershell:

# Set up the objects
$httpSender = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Sender.Http.HttpSender

$clr = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ConfigMgrContentLocationRequest

$cmReply = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationReply

# Discover (the try/catch is in case the ADSI COM
# object cannot be loaded (WinPE).
try
{
   $clr.Discover()
}
catch
{
   $clr.LocationRequest.ContentLocationInfo.IPAddresses.DiscoverIPAddresses() $clr.LocationRequest.ContentLocationInfo.ADSite.DiscoverADSite()
   $clr.LocationRequest.Domain = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationDomain
   $clr.LocationRequest.Domain.Name = $myDomain
   $clr.LocationRequest.Forest = New-Object -TypeName Microsoft.ConfigurationManagement.Messaging.Messages.ContentLocationForest
   $clr.LocationRequest.Forest.Name = $myForest
}

# Define our SCCM Settings for the message
$clr.SiteCode = $ManagementPointSiteCode
$clr.Settings.HostName = $ManagementPoint
$clr.LocationRequest.Package.PackageId = $PackageID
$clr.LocationRequest.Package.Version = $PackageVersion

# If we want to "spoof" a request from a different
# AD Site, we can just pass that AD Site in here.
if ($myCustomAdSite -ne $null)
{
   $clr.LocationRequest.ContentLocationInfo.ADSite = $myCustomAdSite
}

# Validate
$clr.Validate([Microsoft.ConfigurationManagement.Messaging.Framework.IMessageSender]$httpSender)

# Send the message
$cmReply = $clr.SendMessage($httpSender)

# Get response
$response = $cmReply.Body.Payload.ToString()

while($response[$response.Length -1] -ne '>')
{
   $response = $response.TrimEnd($response[$response.Length -1])
}

 Write-Output $response

Enjoi!

Get BitLocker Recovery From Active Directory with Powershell

At the company I currently work for we’ve been using BitLocker since Vista (although Vista install base was rather small). Having moved from another “Pretty Good” Disk encryption technology, one thing we struggled with was Recovery information. While the previous solution had its own server to store all recovery keys, BitLocker links these to the Computer Object. When that computer object is deleted, so is the key. This was a battle for us at first since we have automation in place to remove stale AD Accounts, but–at the time–did not have any method to backup that information. We immediately added a table to a database and as part of our cleanup would backup these Keys.

First: Why not MBAM?

I loosely touched on this in another post regarding TPM Backup information in AD and i’ll give the quick summary again. By the time MBAM cam around, we already had our backup system in place. This was added into a global utility that allowed our technicians to perform AD tasks without AD permissions (Create computer, reset, move ou, etc…). MBAM would have just been “another thing” for us to maintain and manage. With our custom tool and SCCM already in place, MBAM did not provide us any additional benefits. For those of you using MBAM, This may not be for you, however this still provides a great way to quickly grab the information for those ad-hoc requests.

As we move to upgrade our various automation from the tried-and-true VBScript to the ever-so-loved Powershell, Obtaining the BitLocker Recovery Information was an odd one. If you use the RSAT tools and pull up details of a computer object there is a “BitLocker Recovery tab”.

RecoveryB

Each BitLocker Recovery password is an object of the computer account object. The odd thing is that by doing a Get-ADComputer, Recovery information is not returned as a “property” like other items–and where I expected it. As usual, first step was to use a search engine and see what others had done. I found a few results that gave me what i wanted (notably this post) but didn’t like the way it was scripted. The snippet that does the work is here:

$objADObject = get-adobject -Filter * | Where-Object {$_.DistinguishedName -match $objComputer.Name -and $_.ObjectClass -eq "msFVE-RecoveryInformation"}

While this gets the job done it is slow and by slow i mean the combination of “-Filter *” and piping to “Where-Object” is looking at everything before seeing if its the item (droid) that I’m looking for. Recovery Key look-up for my system took approximately 15 seconds for the first result, and didnt finish processing until a few seconds after that.

Let’s step back and look at a BitLocker Recovery object (msFVE-RecoveryInformation); really lets justify the filter “$_.DistinguishedName -match $objComputer.Name”.

Each BitLocker Recovery object is of the Object Class: msFVE-RecoveryInformation. If you look at the DistinguishedName of an object we can see it stems from the Distinguished name of the computer object it is attached to:

DistinguishedName : CN=2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A},CN=WIN7X86PC01,OU=swComputers,DC=swansonglab,DC=com

Unfortunately this is the best way (I’ve found) to tie a recovery object back to the computer account. For those curious, here is what’s contained in a msFVE-RecoveryInformation object:

CanonicalName : swansonglab.com/swComputers/WIN7X86PC01/2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A}
CN : 2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A}
Created : 10/20/2014 1:10:55 PM
createTimeStamp : 10/20/2014 1:10:55 PM
Deleted :
Description :
DisplayName :
DistinguishedName : CN=2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A},CN=WIN7X86PC01,OU=swComputers,DC=swansonglab,DC=com
dSCorePropagationData : {12/31/1600 6:00:00 PM}
instanceType : 4
isDeleted :
LastKnownParent :
Modified : 10/20/2014 1:10:55 PM
modifyTimeStamp : 10/20/2014 1:10:55 PM
msFVE-KeyPackage : {136, 1, 0, 0...}
msFVE-RecoveryGuid : {255, 105, 157, 229...}
msFVE-RecoveryPassword : 465762-121880-049797-598411-533643-549890-128436-549736
msFVE-VolumeGuid : {146, 36, 120, 28...}
Name : 2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A}
nTSecurityDescriptor : System.DirectoryServices.ActiveDirectorySecurity
ObjectCategory : CN=ms-FVE-RecoveryInformation,CN=Schema,CN=Configuration,DC=swansonglab,DC=com
ObjectClass : msFVE-RecoveryInformation
ObjectGUID : d0a15cc8-5f86-42ed-8942-633cec25b6b1
ProtectedFromAccidentalDeletion : False
sDRightsEffective : 15
showInAdvancedViewOnly : True
uSNChanged : 99936
uSNCreated : 99936
whenChanged : 10/20/2014 1:10:55 PM
whenCreated : 10/20/2014 1:10:55 PM

Remember in most cases getting recovery is searched by the GUID. This can be filtered on using Powershell with no problems or computer object linking. The example I’m looking at is getting all recovery objects attached to a computer account.

So my issues with the snippet above (using “-Filter *” and “Where-Object”), again, was time consuming. Since the Recovery Information objects are attached to a computer object we can simply use the computer account’s Distinguished Name as the search base for the command:

# Get Computer Object
> $computer = Get-ADComputer -Filter {Name -eq 'WIN7X86PC01'}

# Get all BitLocker Recovery Keys for that Computer. Note the 'SearchBase' parameter
> $BitLockerObjects = Get-ADObject -Filter {objectclass -eq 'msFVE-RecoveryInformation'} -SearchBase $computer.DistinguishedName -Properties 'msFVE-RecoveryPassword'

# Output the results!
> $BitLockerObjects
DistinguishedName : CN=2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A},CN=WIN7X86PC01,OU=swCompute
rs,DC=swansong,DC=com
msFVE-RecoveryPassword : 465762-121880-049797-598411-533643-549890-128436-549736
Name : 2014-10-20T13:10:38-06:00{E59D69FF-6A3B-42A6-89C0-57A0DA0E302A}
ObjectClass : msFVE-RecoveryInformation
ObjectGUID : d0a15cc8-5f86-42ed-8942-633cec25b6b1

DistinguishedName : CN=2014-10-20T13:11:29-06:00{450547C6-675C-4A61-B276-17CC620D3885},CN=WIN7X86PC01,OU=swCompute
rs,DC=swansong,DC=com
msFVE-RecoveryPassword : 632126-201135-053504-485045-151657-139986-094820-137687
Name : 2014-10-20T13:11:29-06:00{450547C6-675C-4A61-B276-17CC620D3885}
ObjectClass : msFVE-RecoveryInformation
ObjectGUID : 8c3963ea-89ec-4b41-934b-ee6023d9d1e9

DistinguishedName : CN=2014-10-20T13:12:03-06:00{A29D2D47-89D6-4459-B106-40B1F62A04EF},CN=WIN7X86PC01,OU=swCompute
rs,DC=swansong,DC=com
msFVE-RecoveryPassword : 497178-478654-023111-302291-606034-162855-504163-720698
Name : 2014-10-20T13:12:03-06:00{A29D2D47-89D6-4459-B106-40B1F62A04EF}
ObjectClass : msFVE-RecoveryInformation
ObjectGUID : 4a72004e-e76e-4cb3-a828-152011b8b541

Hopefully this has provided some additional information on BitLocker Recovery keys and how to obtain them should you ever need to!

Enjoi!

Powershell: Update Distribution Priority for all Task Sequence Packages

Last week we had a most unfortunate incident. One of our domains we managed had 4 of the 5 drives fail in the SCCM Primary’s Disk Array. This domain fed off of our Packages from our SCCM 2007 environment. While most of the OSD media was created at the Central Site, the Task Sequence and Boot media were created and managed on this domain and on this SCCM Primary Server–Pretty much a total loss.

After the hardware was fixed, we updated the site to SCCM 2012 R2 and began rebuilding the OSD process. Since this new SCCM 2012 site needed to replicate content again, the multitude of packages began distributing. During the outage PC Deployments got behind so we needed a quick way to set the priority of all (50 or so) packages to “high” priority so they would distribute first. Rather than going through each package 1-by-1, I knew Powershell could do this quicker than I could 2 packages manually!

A few Cmdlets to familiarize ourselves with:

  • Get-CMTaskSequence – Gets a Config Manager Task Sequence
  • Get-CMPackage – Gets a Config Manager “Classic” Package
  • Get-CMDriverPackage – Gets a Config Manager Driver Package
  • Get-CMOperatingSystemImage – Gets a Config Manager Operating System Image (Not source media)
  • Get-CMBootImage – Gets a Config Manager Boot Image
  • Get-CMOperatingSystemInstaller – Gets a Config Manager Operating System Source Media Package

First thing is to get the Referenced Task Sequence Packages. This can be done with a quick one liner:

(Get-CMTaskSequence -TaskSequencePackageId AAA######).References.Package

From here we have packages to feed into other cmdlets and modify the priority. But there’s one catch…Depending on the type of package (OSImage, Package, Boot Media, Driver Package, etc…) a different cmdlet is required. To work around this, I went to SQL; while i realize this isn’t a *great* workaround, it does the job.

The following SQL Query can be used to easily get the appropriate Package Type:

[System.Data.SqlClient.SqlConnection]$sms = New-Object System.Data.SqlClient.SqlConnection("Data Source=$SqlServerName;Integrated Security=SSPI;Initial Catalog=$SqlTableName")
        [System.Data.SqlClient.SqlCommand]$qry = New-Object System.Data.SqlClient.SqlCommand("SELECT PackageID, Name, PackageType FROM v_package WHERE PackageID = '$packageId'", $sms)
try
        {
        $sms.Open()
        [System.Data.SqlClient.SqlDataReader]$res = $qry.ExecuteReader()

        while ($res.Read() -eq $true)
        {
            $tmp = @{}
            for ($i=0;$i -lt $res.FieldCount; $i++) {
                $tmp[$res.GetName($i)] = $res.GetValue($i)
            }
            $output += $tmp
            }
        }
        catch
        {
            Write-Host "SQL Connection / Query Failed:`n$_"
        }
        finally
        {
            $sms.Close()
        }

This will return the results into a very nice object to work with: “$output” in this case. Grabbing a column is easy as well: $output.<columnName> ($output.PackageType).

From here we do a switch statement on the “PackageType” value and update the priority. Free Samples:

Standard Package:

$pkg = Get-CMPackage -Id  -ErrorAction SilentlyContinue
$pkg.Priority = 1 #Set to High Priority
Set-CMPackage -InputObject $pkg -ErrorAction SilentlyContinue

Driver Package

$dp = Get-CMDriverPackage -ID $p -ErrorAction SilentlyContinue
$dp.Priority = 1  #Set to High Priority
Set-CMDriverPackage -InputObject $dp -ErrorAction SilentlyContinue

OS Image Package

$osp = Get-CMOperatingSystemImage -Id $p -ErrorAction SilentlyContinue
$osp.Priority = 1  #Set to High Priority
Set-CMOperatingSystemImage -InputObject $osp -ErrorAction SilentlyContinue

Boot Image Package

$bi = Get-CMBootImage -Id $p -ErrorAction SilentlyContinue
$bi.Priority = 1  #Set to High Priority
Set-CMBootImage -InputObject $bi -ErrorAction SilentlyContinue

Operating System Install Package

$osi = Get-CMOperatingSystemInstaller -Id $p -ErrorAction SilentlyContinue
$osi.Priority = 1  #Set to High Priority
Set-CMOperatingSystemInstaller -InputObject $osi -ErrorAction SilentlyContinue

…And that’s just about the basics of it. After using this script to set all of this up, I expanded to allow either a Task Sequence Package ID or an array of Package ID’s. There are some required items including Read Access to your Config Manager Sql Database, and the 2012R2 console has to be installed on the computer (to use the cmdlets).

See the complete script here: https://onedrive.live.com/redir?resid=3D90B836AD7F51CF%21984

Please let me know if you discover any bugs or any improvements as well! Hopefully this will help you out too!

Enjoy!
@ndswansong

TPM Not Found After OSD

Today I experienced something frustrating. After applying OSD on some new hardware I attempted to enable BitLocker (TPM+PIN Configuration). To my surprise I received an error that a valid TPM could not be found. Here is my experience and methodology for troubleshooting a missing TPM.

(1) Check WMI
Using Powershell:

Get-WmiObject Win32_TPM -Namespace root/cimv2/security/MicrosoftTPM | Select IsActivated_InitialValue, IsEnabled_InitialValue, IsOwned_InitialValue | Format-List

Usually you’d see something like this:

IsActivated_InitialValue : True
IsEnabled_InitialValue   : True
IsOwned_InitialValue     : True

(2) Check TPM MMC Console
If and when WMI is blank I move on to the TPM MMC console snap-in (tpm.msc).

(3) Check BIOS
At this point I’ve determined the TPM isn’t visible to the Operating System; It happens! On most BIOS you’ll have settings whether or not the Operating System can see and/or manage the TPM Device. Boot into the BIOS, look for a security section and check the TPM Status. In my instance this looked good! TPM was listed as Enabled and Activated. I rebooted back to the OS, repeated steps 1 and 2…both still with the same result.

(4) Vendor Software
When it comes to using vendor-provided installers/software/executables to install drivers, I typically have one rule: I DON’T! In my experience (Dell, Lenovo, HP, Samsung, MS Surface) Plug-n-Play will identify and capture 99% of the hardware without the necessity to install the vendor’s software. In this instance this was an Infineon TPM device so I grabbed the driver CD, extracted and ran. Unless absolutely necessary, I’ll usually use the software to install only the driver where possible. A lot of driver installations will also provide an application that tromps over the built in Windows functions with their own; Bloatware, Crapware…call it what you want, I find it extremely unnecessary. The most infuriating being the old “HP Wireless Assistant” which was a clunky “remix” of Windows’ normal Wireless Connection Menu and tray icon–I found this to be slow, clunky, and down right unnecessary!

Back to the TPM…I launched the “Infineon TPM Professional Package”. I select custom install hoping to see “Driver”. Instead I see a bunch of extra stuff I don’t need and when I went to tell the installer to “not install” a component, I saw this wasn’t an option…It would appear that in order to get an Infineon TPM device seen to the OS, I have to use their crappy application to “manage and control” it. Not gonna happen!

As I contemplated what to do next I eventually did what I probably should have done at step (1)…

Check Device Manager
Usually, device manger would show a TPM device under the “security” category as seen:

TPM_devmgmt_cropped

 

 

Unfortunately, not found! I then started digging in Device Manager and eventually stumbled across “Infineon Trusted Platform Module” under “System Devices”. That explains why the TPM MMC couldn’t find it!

TPM_inSYS

Right-Click -> Uninstall (making sure to check “Delete the driver software for this device”.

uninstallTPM

Richt-Click –> “Scan for Hardware Changes”

Just like magic, the TPM was detected and placed in the “Security Devices” category. TPM.msc detected it. BitLocker was enabled and there was much rejoicing. I then removed the “tpm driver” from the Driver Package, updated distribution points and all was well!

 

My hope is that this will save some of you from additional headaches if this comes up in your OSD world.

 

Enjoy!

Search the Windows Store from Powershell

Let’s look at the Windows Store. Lots of apps (and growing) and with all of us being the Powershell-hungry people we are, Let’s search the store with Powershell!

I used Fiddler to grab the search string. Looking at the URL call, I image that this may be changed/updates as time goes by, however, since we don’t have an official Windows Store API to work with, this will have to do. The main web request I found and used was:
https://next-services.apps.microsoft.com/search/6.3.9600-0/776/en-US_en-US/m/US/c/US/il/en-US/cp/10005001/query/cid/0/pf/1/pc/0/pt/x64/af/0/lf/1/s/0/2/pn/0/pgc/-1?phrase=Netflix

Obviously replacing “Netflix” at the end will allow us to put in our own string to search with. For example, if I wanted to search for Zillow we could simply modify the string:
https://next-services.apps.microsoft.com/search/6.3.9600-0/776/en-US_en-US/m/US/c/US/il/en-US/cp/10005001/query/cid/0/pf/1/pc/0/pt/x64/af/0/lf/1/s/0/2/pn/0/pgc/-1?phrase=Zillow

The search results come back in XML. Within Powershell we can use the Invoke-WebRequest to get the response and then convert over to an XML object.

$appResult = Invoke-WebRequest -Uri "https://next-services.apps.microsoft.com/search/6.3.9600-0/776/en-US_en-US/m/US/c/US/il/en-US/cp/10005001/query/cid/0/pf/1/pc/0/pt/x64/af/0/lf/1/s/0/2/pn/0/pgc/-1?phrase=Netflix"

[xml]$appResultXml = $appResult.Content

Note: When using your own search string, spaces need to be formatted in a “URL” friendly way (For example, a space ends up being %20). With Powershell we can use the static method EscapeDataString to give us a good string:

$newFormattedString = [System.Uri]::EscapeDataString("Hello I Have Spaces")
Write-Host $newFormattedString
> Hello%20I%20Have%20Spaces

If you want, you can pipe $appResultXml to the Out-File cmdlet and look at the XML for a better visual understanding. Dig down to the “Pt” elements (there should be more than one). Each Pt element is a result in our search. The more results, the more Pt’s. Digging into each Pt elemet we get some additional information on each app in the result. If we go back to powershell, we can easily loop through these items and give us some additional data:

# Loop through the Results
foreach ($appResObj in $appResultXml.rslt.ptl.pts.pt)
{
        write-host "ProductFamily Name:`t" $($appResObj).pfn
        write-host "Language:`t" $($appResObj).L
        write-host "Title:`t" $($appResObj).T
        write-host "Age Rating:`t" $($appResObj).Wr
        write-host "Average Rating:`t" $($appResObj).Sr
        write-host "Total Reviews:`t" $($appResObj).src
        write-host "Currency Code:`t" $($appResObj).Cc
        write-host "Cost:`t" $($appResObj).p
        write-host "Category Name:`t" $($($appResObj).n).i
        write-host "Category ID:`t" $($($appResObj).c).n
        write-host "Developer Name:`t" $($appResObj).Dev
        write-host "Developer ID:`t" $($appResObj).DevI
        write-host "`n`n"
}

In our Zillow response from above, the output will look like this:

ProductFamily Name:	 Zillow.Zillow_nxemgnmz6chhm
Language:	 en
Title:	 Zillow
Age Rating:	 7
Average Rating:	 4
Total Reviews:	 332
Currency Code:	 USD
Cost:	 0.00
Category Name:	 Lifestyle
Category ID:	 14
Developer Name:	 Zillow
Developer ID:	 844427398339648

ProductFamily Name:	 60695TheRobot.Housevalue_8m7qmqa133yjj
Language:	 en-us
Title:	 House value
Age Rating:	 12
Average Rating:	 3.4
Total Reviews:	 28
Currency Code:	 USD
Cost:	 0.00
Category Name:	 Finance
Category ID:	 17
Developer Name:	 The Robot
Developer ID:	 306107898116081

ProductFamily Name:	 51189ApoorvUpadhyay.Neighbourhood_2z112j43hyvdw
Language:	 en-us
Title:	 Neighbourhood
Age Rating:	 7
Average Rating:	 1
Total Reviews:	 1
Currency Code:	 USD
Cost:	 1.49
Category Name:	 Tools
Category ID:	 19
Developer Name:	 ApoorvUpadhyay
Developer ID:	 985156469124598

and two more....(Shortened for brevity)

For me the benefit of this is being able to grab the “Category” for an app programmatically. If we have SCCM 2012 R2, we can easily use that inventoried information and locate the additional information. I use the query down below. Note the where clause to exclude built in apps that are not in the Windows Store. I’m also doing a “distinct” because I don’t care about the version of each (The ApplicationName and FamilyName are consistent even when the version changes).

select distinct
	ApplicationName0,
	FamilyName0,
	PublisherId0
from v_GS_WINDOWS8_APPLICATION
where IsFramework0 = 0

If we use these results you’ll need to take “ApplicationName” and split the string. This string is in a format <Developer>.<Application>. Searching using the URL above, using the Publisher name may not return what we want (at least in my findings).

$appName = "4DF9E0F8.Netflix"                   # ApplicationName0
$appSearchableName = $appName.Split('.')[1]     # Since ApplicationName0 is <Developer>.<Application> we only want <Application> for searching

After the simple Split, we’ll have the 2nd half of the ApplicationName string (“Netflix” in the example above.) Add in the new $appSearchableName to the WebRequest above, and check it out! Today, I’m grabbing the Category data (as well as the rest of the information) into a database and keying off of the FamilyName column in the SCCM SQL View v_GS_Windows8_Applications. If you do want to match this data up with what is in SCCM, I’d recommend nesting an if statement to check the returned “pfn” (product family name) with the “FamilyName” column in SCCM.

Things to Consider:

  • The search URL I’m using appears to have some version and architecture information in it. My guess is this will change but only time will tell.
  • I did find some applications that may be thrown off by my split command. For example, if the application or publisher name has “.NET” in the name the additional “.” will throw this off.
  • I have had instances where the application is not found by the search string i’m generating. I’m experimenting with some RegEx and other string manipulators to try to add spaces or search until the desired result is found.

Using this process, I was able to pull in about 75% of the inventoried apps within SCCM that we have (out of ~300 distinct apps). It’s not perfect but I am still working on this, so hopefully I can provide some meaningful updates soon!

Enjoy!

Easily Identify Windows To Go In Your Scripts

Updated Powershell and vbscript Functions 8/16/2015

When Windows 8 was released into the wild this new supported feature “Windows To Go” was released with it. Now that Windows 8.1 is here the “To Go” OS was upgraded and released just the same.

From a scripting perspective, being able to differentiate between an internal OS drive and an external “To Go” configuration may be desired. Luckily with Windows 8 and up, Microsoft added a property to the Win32_OperatingSystem WMI Class to make this easier.

vbScript: (Updated 8/16/2015)

Function IsPortableOS(ComputerName)
   Dim SWBemlocator, objWMIProc
   Dim objOS, colOS, strOSVer, bIsPortableOS

   ' Create connection objects
   Set SWBemlocator = CreateObject("WbemScripting.SWbemLocator")
   Set objWMIProc = SWBemlocator.ConnectServer(ComputerName, "root\CIMV2")
   Set colOS = objWMIProc.ExecQuery("Select * from Win32_OperatingSystem")

   For Each objOS In colOS
      strOSVer = Left(objOS.Version,3)
      

      If ((strOSVer > "6.1") OR (LEFT(strOSVer,2) = "10"))Then
         ' Only return a value if Win8 or above.
         IsPortableOS = objOS.PortableOperatingSystem
      Else
      	' Property doesn't exist in Win7 and before
      	IsPortableOS = False
      End If
   Next
End Function

Powershell: (Updated 8/16/2015)

function Get-OperatingSystemInformation()
{
   [cmdletbinding()]
   param(
      [Parameter(Mandatory=$true)]
      [string] $ComputerName
   )

   try
   {
      $wmiObjOperatingSystem = Get-WmiObject Win32_OperatingSystem -ComputerName $ComputerName
   }
   catch
   {
      Write-Verbose -Message "Unable to get Operating System from WMI for computer $computer"
      return [HashTable]::new($null)
   }
	
   # Cast the version property to type System.Version
   [System.Version]$vOsVersion = $wmiObjOperatingSystem.Version

   # We do a try here because with Windows 7 This property does not exist.
   try
   {
      if ($wmiOsObject.PortableOperatingSystem)
      {
         $boolIsPortableOs = $true
      }
      else
      {
         $boolIsPortableOs = $false
      }
   }
   catch
   {
      $boolIsPortableOs = $false
   }

   return $htOs = @{
      'caption'=$wmiObjOperatingSystem.Caption;
      'fullversion'=$wmiObjOperatingSystem.Version;
      'osarchitecture'=$wmiObjOperatingSystem.OSArchitecture;
      'versionMajor'=$vOsVersion.Major;
      'versionMinor'=$vOsVersion.Minor;
      'versionBuild'=$vOsVersion.Build;
      'isPortableOS'=$boolIsPortableOs
   }
}

One important detail to note is that we need to do an OS version evaluation before checking the PortableOperatingSystem property. Until Windows 8 this property didn’t exist. You could put “$wmiOperatingSystem.PortableOperatingSystem” in a try/catch if you wanted to as well.

Now its not that often that you’ll have to detect or identify a Windows To Go configuration but if you’re like me and you manage a TPM BitLocker configuration for standard laptops those policies and the To Go sticks will not play nicely. You can simply create a Group Policy WMI filter for portable operating systems:

Select * from Win32_OperatingSystem where PortableOperatingSystem = "True"

Enjoy!
-NDS