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!

Using DiskPart with C#

I’m in the process of writing a little utility for our technicians to create a bootable USB device from an ISO. This will be primarily used with our SCCM OSD Media, DaRT media, and will probably find several other uses in time. In developing this I ran into an issue….we like to completely reformat the disk when media is created; probably not completely necessary but this seems to help minimize the issues we saw.

There are several articles out and about mentioning different COM and WMI interfaces you can use, but they seemed a bit messy to me. My love for diskpart has grown to an extent and therefore decided to leverage it…but how.

I didn’t like the idea of writing a file and then referencing a diskpart “script” using the “/s” command so I continued my experiment. For those of you who always use the interactive DiskPart console, you can list all your commands into a txt file and then specify this into the command line of a diskpart script.

For example:

<contents of diskpart.txt>

   select disk 1
   clean
   create partition primary
   select partition 1
   format fs=nfts quick
   active
   assign
   exit

I can then use this file on the command line as so:

> diskpart.exe /s diskpart.txt

and all my commands run as if i typed them individually in the diskpart console.

With Powershell, I can use a here-string for this and then pipe the string to diskpart:

$diskpartString = @"
     select disk 1
     clean
     create partition primary
     select partition 1
     format fs=nfts quick
     active
     assign
     exit
   "@

$diskpartString | diskpart

With C# I’m using a method to do this that mimics the same way using an instance of the Process Class:

 

using System.Diagnostics;

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      Process p = new Process();                                    // new instance of Process class
      p.StartInfo.UseShellExecute = false;                          // do not start a new shell
      p.StartInfo.RedirectStandardOutput = true;                    // Redirects the on screen results
      p.StartInfo.FileName = @"C:\Windows\System32\diskpart.exe";   // executable to run
      p.StartInfo.RedirectStandardInput = true;                     // Redirects the input commands
      p.Start();                                                    // Starts the process
      p.StandardInput.WriteLine("select disk 2");                   // Issues commands to diskpart
      p.StandardInput.WriteLine("clean");                           //   |
      p.StandardInput.WriteLine("create partition primary");        //   |
      p.StandardInput.WriteLine("select partition 1");              //   |
      p.StandardInput.WriteLine("format fs=fat32 quick");           //   |
      p.StandardInput.WriteLine("active");                          //   |
      p.StandardInput.WriteLine("assign");                          //   |
      p.StandardInput.WriteLine("exit");                            // _\|/_
      string output = p.StandardOutput.ReadToEnd();                 // Places the output to a variable
      p.WaitForExit();                                              // Waits for the exe to finish

      Console.WriteLine(output);                                    // prints the output
    }
  }
}

While this is merely one way of doing this, I like it because it’s self contained and doesn’t require any additional files or api calls. Having the diskpart executable is required. Luckily here, we bake this into our OS Images so we are safe! Perhaps the “dirtiest” part of this is the fact that if we want to pull information from this output, it gets messy. In my instance I just need the volume letter assigned which I’m pulling from WMI.

I’ve instituted a class library to handle most of DiskPart’s commands in this way. Once it has matured a bit i’ll post this back here.

Hopefully, this helps someone somewhere do this!

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

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!

Change BitLocker PIN with Complexity

BitLocker Enabled for OS Drives? Check!
Using PIN authentication? Check!
Enforcing PIN Complexity? …

True, we get a lot of options with BitLocker in Windows. One thing that we struggled with is the inability to require any kind of complexity with the PIN. Through Group Policy we can set a minimum length which helps but what if I need to enforce alternate complexity? Say, require One Upper Case, One Lower Case, and One Number?

Well, I’ve done that work for you so sharpen your copy/paste skills and lets have at it!

Before this will work completely, make sure that you’ve enabled the Group Policy “Allow Enhanced PINs for Startup”

We’ve used this code since our first step into Windows 7 and vbscript was the winner. To add a GUI, we dropped the vbscript into an HTA and had a functional utility. Then the loveable storm I call Powershell came along, and naturally a rewrite was inevitable–Integrating a Windows Form was an obvious “plus” as well. These were tested on Windows 7 – Windows 8.1 (including Windows To-Go–Yeah, it’s a bit different for Portable OS’s).

Both utilities break apart the logic in some easy to call/use functions/subs. I’ve also added some nice messages that the user will see to help maintain their complexity in the full scripts (linked at the bottom). Hopefully if you don’t need the functionality of the utilities you can use the code snippets for other projects.

Check Length:

Powershell:

function CheckStringLength([int]$lowerInt, [int]$upperInt, [string]$stringValue)
{
    If ($stringValue.Length -lt $lowerInt)
    { return -1 }
    ElseIf ($stringValue.Length -gt $upperInt)
    { return 1 }
    else
    { return 0 }
}

vbScript:

Function CheckStringLength(lowerInt, upperInt, stringValue)
    If Len(stringValue) < lowerInt Then CheckStringLength = -1 Exit Function ElseIf Len(stringValue) > upperInt Then
        CheckStringLength = 1
        Exit Function
    Else
        CheckStringLength = 0
    End If

End Function

Require Uppercase:

Powershell:

If (-Not ($stringValue.CompareTo($stringValue.ToLower())))
{ return $false }

vbScript:

Function ContainsUpperCase(stringValue)
    If stringValue <> LCase(stringValue) Then
        ' Upper Case passed
        ContainsUpperCase = True
        Exit Function
    End If

    ContainsUpperCase = False

End Function

Require Lowercase:

Powershell:

If (-Not ($stringValue.CompareTo($stringValue.ToUpper())))
    { return $false }

vbScript:

Function ContainsLowerCase(stringValue)
    If stringValue <> UCase(stringValue) Then
        ' Lower case Passed
        ContainsLowerCase = True
        Exit Function
    End If	

    ContainsLowerCase = False

End Function

 

Require Number:

Powershell:

function ContainsNumber([string]$stringValue)
{
    foreach ($c in $stringValue.ToCharArray())
    {
        If ($c -match "[0-9]")
        {
            return $true
        }
    }

    return $false
}

vbScript:

Function ContainsNumber(stringValue)

    Dim iNum

    For iNum = 1 To Len(stringValue)
        If IsNumeric(Mid(stringValue, iNum, 1)) Then
            ContainsNumber = True
            Exit Function
        End If
    Next

    ContainsNumber = False

End Function

 

Prevent Special Characters:

Powershell:

function ContainsSpecialCharacter([string]$stringValue)
{
    $charArray = [int[]][char[]]$stringValue

    foreach ($c in $charArray)
    {
        if (($c -lt 32) -or ($c -gt 128))
        {
            return $true
        }
    }

    return $false
}

vbScript:

Function ContainsSpecialCharacter(stringValue)

    Dim iChar, cChar

    For iChar = 1 To Len(stringValue)
        cChar = Mid(stringValue, iChar, 1)

        ' Check if Character is in range
        If Asc(cChar) < 32 Or Asc(cChar) > 128 Then
            ContainsSpecialCharacter = True
            Exit Function
        End If
    Next

    ContainsSpecialCharacter = False

End Function

 

Complete scripts:

vbScript/HTA: https://skydrive.live.com/redir?resid=3D90B836AD7F51CF%21786
Powershell: https://skydrive.live.com/redir?resid=3D90B836AD7F51CF%21785