SCCM Collection Based on Baseline Compliance

select SMS_R_System.ResourceId,
SMS_R_System.ResourceType,
SMS_R_System.Name,
SMS_R_System.SMSUniqueIdentifier,
SMS_R_System.ResourceDomainORWorkgroup,
SMS_R_System.Client
from
SMS_R_System inner join SMS_G_System_CI_ComplianceState on SMS_G_System_CI_ComplianceState.ResourceID = SMS_R_System.ResourceId
Where
SMS_G_System_CI_ComplianceState.ComplianceStateName = "<ComplianceState>"
and SMS_G_System_CI_ComplianceState.LocalizedDisplayName = "<BaselineName>"
and SMS_G_System_CI_ComplianceState.CI_UniqueID = "<CI Unique ID>"

<ComplianceState> can be ‘compliant’ or ‘non-compliant’

<BaselineName> is the displayname of the Configuration Baseline

<CI Unique ID> is the unique ID e.g. ‘Scope_xxxx’

Source

Timing how long each group in a Task Sequence is Taking

I wanted a way to determine how long steps in an OS deployment TS were taking, until recently I thought the only window we had into this was running scripts as part of OSD or looking at Status Messages.

Then I found this view in the SCCM DB – v_TaskExecutionStatus

Like most views and tables in SCCM, each row has a ResourceID and contains information which looks very similar to the status messages. However, I have noticed that the Action Output is sometimes truncated (cut-off), and there are multiple rows for some steps.

Each row has fields called ActionName, GroupName, and ExecutionTime. Using these fields we should be able to build up a picture of how long each step and group is taking.

Initially, I started by querying all rows for a specific client which I knew had run a TS lately. This gave me all the information I needed, but if the client had run multiple task sequences it had entries for all the runs.

SELECT
	*
FROM
	v_TaskExecutionStatus AS ts
	inner join
	v_R_System AS s
		ON ts.ResourceID = s.ResourceID
WHERE
	s.Netbios_Name0 = '#HOSTNAME#'
ORDER BY
	ExecutionTime ASC

I then noticed that some steps would be listed incorrectly as Step 0, this was messing up the script I wrote to compare the time between one row and the next. I noticed each type of step was accompanied by a LastStatusMessageID field, using these IDs we can limit the query down to something more useful :

SELECT
	a.PackageName,
	s.Netbios_Name0,
	ts.ExecutionTime,
	ts.step,
	ts.GroupName,
	ts.LastStatusMessageID
FROM
	v_TaskExecutionStatus AS ts
	inner join
	v_R_System AS s
		ON ts.ResourceID = s.ResourceID
	inner join
	v_AdvertisementInfo as a
		on ts.AdvertisementID = a.AdvertisementID
WHERE
	s.Netbios_Name0 = '#HOSTNAME#'
	AND
	(LastStatusMessageID IN (11124,11127,11143)
	OR
	(LastStatusMessageID = 11140 AND step = 0))

ORDER BY
	ExecutionTime ASC

If the hostname / client has only run 1 TS, this query will return data on when it started (11140 & Step 0) as well as when each group started and completed. Using this and a bit of powershell we can establish the duration that each group takes to run.

This is just a snippet of the script on GitHub :


$table = (invoke-sql -dataSource $sccmsqlserver -database $sccmsqldb -sqlCommand $query).Tables[0]

$hash = DataTableToHashTable -table $table

$groupoutput = @()
foreach ($line in $hash)
{
    $thisgroupname = $line.GroupName
    if($line.LastStatusMessageID -eq 11124)
    {
        ## Start of Group
        $obj = [pscustomobject]@{
            GroupName = $line.GroupName
            ParentGroup = $null
            StartTime = $line.ExecutionTime
            EndTime = $null
            Duration = $null
            DurationSeconds = $null
        }
        $groupoutput += $obj
    } elseif ($line.LastStatusMessageID -eq 11127)
    {
        ## End of Group
        $findgroup = $groupoutput | ?{$_.GroupName -eq $line.GroupName}
        $findgroup.EndTime = $line.ExecutionTime
    }
}

## Calculate Duration of Each Group
$groupoutput | % {$_.Duration = $_.EndTime - $_.StartTime}

## Calculate Duration in Seconds
$groupoutput | % {$_.DurationSeconds = $_.Duration.TotalSeconds}
#endregion

If you output $groupoutput to a gridview you should see something like this :

ts_group_duration1

This is just an initial script, which I will improving in the future. Groups are often nested in a TS and we need to be able to see and understand this in the output. I also want to extend the extend the script to show the duration of individual steps.

Cireson ConfigMgr Ticker WMI Permissions Fix

We encountered an issue with the Cireson ConfigMgr Ticker App recently where it stopped working on some clients. The log was reporting WMI permissions issues…

ConfigMgr Ticker WMI Log Errors
Errors in the log caused by WMI permission problems.

I had a quick look on the product forum and found someone with the same issue.

The problem is the Ticker App install adds a permission to the following WMI Class, which at times SCCM resets back to default.

ROOT\ccm\Policy\Machine\RequestedConfig

You can check the permissions on this class using “wmimgmt.msc” -> right click on “WMI Control (local)” -> Properties -> Security Tab -> Expand tree to the class above -> Security. It should look like the screenshot below with the “HOSTNAME\Users” group having the “Enable Account” permission.

Screenshot of wmimgmt.msc permissions for Cireson ConfigMgr Ticker
Screenshot of wmimgmt.msc permissions for Cireson ConfigMgr Ticker

In certain circumstances, SCCM client repair and Windows 10 In-Place upgrade changes the permissions on this WMI class and it reverts back to default (the Users group is removed.) This breaks the Cireson Ticker App client.

Using a few blog posts I created a script to detect and remediate these permissions using a Configuration Manager Baseline.

Create a Configuration Item with a discovery script and remediation script

Discovery Script :
$namespace = "ROOT\ccm\Policy\Machine\RequestedConfig"
$computer = "localhost"
$compliance = "No"

Function Get-PermissionFromAccessMask($accessMask) {
    $WBEM_ENABLE            = 1
	$WBEM_METHOD_EXECUTE         = 2
    $WBEM_FULL_WRITE_REP           = 4
    $WBEM_PARTIAL_WRITE_REP     = 8
    $WBEM_WRITE_PROVIDER          = 0x10
    $WBEM_REMOTE_ACCESS            = 0x20
    $READ_CONTROL = 0x20000
    $WRITE_DAC = 0x40000

    $WBEM_RIGHTS_FLAGS = $WBEM_ENABLE,$WBEM_METHOD_EXECUTE,$WBEM_FULL_WRITE_REP,$WBEM_PARTIAL_WRITE_REP,$WBEM_WRITE_PROVIDER,$WBEM_REMOTE_ACCESS,$WBEM_RIGHT_SUBSCRIBE,$WBEM_RIGHT_PUBLISH,$READ_CONTROL,$WRITE_DAC
    $WBEM_RIGHTS_STRINGS ="Enable","MethodExecute","FullWrite","PartialWrite","ProviderWrite","RemoteAccess","Subscribe","Publish","ReadSecurity","WriteSecurity"

    $permission = @()
    for ($i = 0; $i -lt $WBEM_RIGHTS_FLAGS.Length; $i++) {
        if (($accessMask -band $WBEM_RIGHTS_FLAGS[$i]) -gt 0) {
            $permission += $WBEM_RIGHTS_STRINGS[$i]
        }
    }
    $permission
}

$INHERITED_ACE_FLAG = 0x10

$invokeparams = @{Namespace=$namespace;Path="__systemsecurity=@";Name="GetSecurityDescriptor";ComputerName=$computer}

if ($credential -eq $null) {
    $credparams = @{}
} else {
    $credparams = @{Credential=$credential}
}

$output = Invoke-WmiMethod @invokeparams @credparams
if ($output.ReturnValue -ne 0) {
    throw "GetSecurityDescriptor failed: $($output.ReturnValue)"
}

$acl = $output.Descriptor
$usercoll = @()
foreach ($ace in $acl.DACL) {
    $user = New-Object System.Management.Automation.PSObject
    $user | Add-Member -MemberType NoteProperty -Name "Name" -Value "$($ace.Trustee.Domain)\$($ace.Trustee.Name)"
    $user | Add-Member -MemberType NoteProperty -Name "Permission" -Value (Get-PermissionFromAccessMask($ace.AccessMask))
    $user | Add-Member -MemberType NoteProperty -Name "Inherited" -Value (($ace.AceFlags -band $INHERITED_ACE_FLAG) -gt 0)
    if (($user.Name -eq "BUILTIN\Users") -and ($user.Permission -eq "Enable"))
    {
        $compliance = "Yes"
    }
}
$compliance
Remediation Script :
$namespace = "ROOT\ccm\Policy\Machine\RequestedConfig"
$operation = "Add"
$account = "BUILTIN\Users"
$allowInherit = $false
$deny = $false
$computer = "localhost"
[string[]] $permissions = "Enable"

$ErrorActionPreference = "Stop"

Function Get-AccessMaskFromPermission($permissions) {
 $WBEM_ENABLE = 1
 $WBEM_METHOD_EXECUTE = 2
 $WBEM_FULL_WRITE_REP = 4
 $WBEM_PARTIAL_WRITE_REP = 8
 $WBEM_WRITE_PROVIDER = 0x10
 $WBEM_REMOTE_ACCESS = 0x20
 $WBEM_RIGHT_SUBSCRIBE = 0x40
 $WBEM_RIGHT_PUBLISH = 0x80
 $READ_CONTROL = 0x20000
 $WRITE_DAC = 0x40000

 $WBEM_RIGHTS_FLAGS = $WBEM_ENABLE,$WBEM_METHOD_EXECUTE,$WBEM_FULL_WRITE_REP,$WBEM_PARTIAL_WRITE_REP,$WBEM_WRITE_PROVIDER,$WBEM_REMOTE_ACCESS,$READ_CONTROL,$WRITE_DAC
 $WBEM_RIGHTS_STRINGS ="Enable","MethodExecute","FullWrite","PartialWrite","ProviderWrite","RemoteAccess","ReadSecurity","WriteSecurity"

 $permissionTable = @{}

 for ($i = 0; $i -lt $WBEM_RIGHTS_FLAGS.Length; $i++) {
 $permissionTable.Add($WBEM_RIGHTS_STRINGS[$i].ToLower(),$WBEM_RIGHTS_FLAGS[$i])
 }

 $accessMask = 0

 foreach ($permission in $permissions)
 {
 if (-not $permissionTable.ContainsKey($permission.ToLower())) {
 throw "Unknown permission: $permission`nValid permissions: $($permissionTable.Keys)"
 }
 $accessMask += $permissionTable[$permission.ToLower()]
 }
 $accessMask
}

if ($PSBoundParameters.ContainsKey("Credential")) {
 $remoteparams = @{ComputerName=$computer;Credential=$credential}
} else {
 $remoteparams = @{}
}

$invokeparams = @{Namespace=$namespace;Path="__systemsecurity=@"} + $remoteParams

$output = Invoke-WmiMethod @invokeparams -Name GetSecurityDescriptor
if ($output.ReturnValue -ne 0) {
 throw "GetSecurityDescriptor failed: $($output.ReturnValue)"
}

$acl = $output.Descriptor
$OBJECT_INHERIT_ACE_FLAG = 0x1
$CONTAINER_INHERIT_ACE_FLAG = 0x2

$computerName = (Get-WmiObject @remoteparams Win32_ComputerSystem).Name

if ($account.Contains('\')) {
 $domainaccount = $account.Split('\')
 $domain = $domainaccount[0]
 if (($domain -eq ".") -or ($domain -eq "BUILTIN")) {
 $domain = $computerName
 }
 $accountname = $domainaccount[1]
} elseif ($account.Contains('@')) {
 $domainaccount = $account.Split('@')
 $domain = $domainaccount[1].Split('.')[0]
 $accountname = $domainaccount[0]
} else {
 $domain = $computerName
 $accountname = $account
}

$getparams = @{Class="Win32_Account";Filter="Domain='$domain' and Name='$accountname'"} + $remoteParams

$win32account = Get-WmiObject @getparams

if ($win32account -eq $null) {
 throw "Account was not found: $account"
}

switch ($operation) {
 "add" {
 if ($permissions -eq $null) {
 throw "-Permissions must be specified for an add operation"
 }
 $accessMask = Get-AccessMaskFromPermission($permissions)

 $ace = (New-Object System.Management.ManagementClass("win32_Ace")).CreateInstance()
 $ace.AccessMask = $accessMask
 if ($allowInherit) {
 $ace.AceFlags = $OBJECT_INHERIT_ACE_FLAG + $CONTAINER_INHERIT_ACE_FLAG
 } else {
 $ace.AceFlags = 0
 }

 $trustee = (New-Object System.Management.ManagementClass("win32_Trustee")).CreateInstance()
 $trustee.SidString = $win32account.Sid
 $ace.Trustee = $trustee

 $ACCESS_ALLOWED_ACE_TYPE = 0x0
 $ACCESS_DENIED_ACE_TYPE = 0x1

 if ($deny) {
 $ace.AceType = $ACCESS_DENIED_ACE_TYPE
 } else {
 $ace.AceType = $ACCESS_ALLOWED_ACE_TYPE
 }

 $acl.DACL += $ace.psobject.immediateBaseObject
 }

 "delete" {
 if ($permissions -ne $null) {
 throw "Permissions cannot be specified for a delete operation"
 }

 [System.Management.ManagementBaseObject[]]$newDACL = @()
 foreach ($ace in $acl.DACL) {
 if ($ace.Trustee.SidString -ne $win32account.Sid) {
 $newDACL += $ace.psobject.immediateBaseObject
 }
 }

 $acl.DACL = $newDACL.psobject.immediateBaseObject
 }

 default {
 throw "Unknown operation: $operation`nAllowed operations: add delete"
 }
}

$setparams = @{Name="SetSecurityDescriptor";ArgumentList=$acl.psobject.immediateBaseObject} + $invokeParams

$output = Invoke-WmiMethod @setparams
if ($output.ReturnValue -ne 0) {
 throw "SetSecurityDescriptor failed: $($output.ReturnValue)"
}

Settings for the CI :

baseline_config

Remove annoying Cortana Audio after 1703 OSD

When you build a W10 1703 machine with SCCM OSD, you may notice that once its complete you hear an audile voice saying something like ‘OK I got connected let’s check for updates’.

You can disable this with a simple command line task sequence step  :

reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\OOBE" /v DisableVoice /t REG_DWORD /d 1 /f

SCCM Application Dependency Graph / Tree

Sometimes we have very complicated apps we have to deploy and need to view the application dependency tree to help investigate failed deployments. I found the built in view in the SCCM console clunky, buggy and annoying. So I wrote a small script to query WMI, list app dependencies and then output this visually using graphviz dot language.

The first thing the script does is query all the apps on your site server WMI for a search term. It then displays a list of applications in a grid view. You then choose which Application you want to create a dependency graph for. Once you have chosen one, the script will recurse through all the dependencies and build a graph.

It’s still in its infancy, it doesn’t highlight AND / OR relations and may have some redundant code that needs tiding up… But I find it useful to demonstrate to support staff why some applications take a while to deploy due to the complexity and amount of pre-reqs required!

  • Install graphviz for windows from here
  • Add an entry in your PATH environment varibale to the location of dot.exe – on Windows 10 for me it was : “C:\Program Files (x86)\Graphviz2.38\bin”
  • Download the script from Github Gist Here
  • Edit the variables in the script
    • $server – should be the management point server with the WMI Provider for your SCCM Site
    • $sitecode – should be the sitecode for your site
    • $graph_filetype – filetype of the output (I’ve only tested png, svg and pdf so far…)
    • $query – should be an application search string, the first thing the script will do is search and list the applications found using this. You can then choose which app you want to see the dependency tree for.
  • Run the script!
  • You should get two output files in the output directory
    • AppName.gv
    • AppName.(pdf/png/svg – depending on selected output filetype)
An example output png from the SCCM App Dependency Graph script.
An example output png from the SCCM App Dependency Graph script.


##############################################
############## Script Info ###################
##############################################
## Created By : Dan Cook 2017 ########
##############################################
<#
Version Info :
0.1 – 25/05/2017 – Creates a graphvis diagram and accompanying DOT code file
for an SCCM application dependency tree, by querying the
primary server WMI
0.1.1 – TODO – Make the console output tidier. Use the Write-Color function to add to the console output
0.2 – TODO – Script needs to handle more dependencies better, not all the dependencies are "Automatically Installed". Some are either / OR etc…
#>
#region Variables
$query = "QueryString" # Query used as initial search string
$server = "ServerName" # SCCM Primary Site Server name (may need to be FQDN)
$sitecode = "PS1" # Site code
$basepath = "C:\Path\" # Path for output
## Output graphvis type
## (png, svg, pdf) tested so far
$graph_filetype = "png"
#endregion
function Write-Color([String[]]$Text, [ConsoleColor[]]$Color = "White", [int]$StartTab = 0, [int] $LinesBefore = 0,[int] $LinesAfter = 0) {
$DefaultColor = $Color[0]
if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host "`n" -NoNewline } } # Add empty line before
if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host "`t" -NoNewLine } } # Add TABS before text
if ($Color.Count -ge $Text.Count) {
for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host $Text[$i] -ForegroundColor $Color[$i] -NoNewLine }
} else {
for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host $Text[$i] -ForegroundColor $Color[$i] -NoNewLine }
for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host $Text[$i] -ForegroundColor $DefaultColor -NoNewLine }
}
Write-Host
if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host "`n" } } # Add empty line after
}
function IsNull($objectToCheck) {
if ($objectToCheck -eq $null) {
return $true
}
if ($objectToCheck -is [String] -and $objectToCheck -eq [String]::Empty) {
return $true
}
if ($objectToCheck -is [DBNull] -or $objectToCheck -is [System.Management.Automation.Language.NullString]) {
return $true
}
return $false
}
# Empty Hash table to cache CIID and AppName
$CI_NameTable = @{}
# Empty array to cache SMS_AppDependenceRelation WMI Class objects
$AppDependenceTable = New-Object System.Collections.Generic.List[pscustomobject]
# Colours to use for the different levels
$levelColors = @{
1 = "deepskyblue2"
2 = "brown3"
3 = "goldenrod"
4 = "deeppink3"
5 = "coral3"
}
# First couple of lines of DOT output syntax
$output = @"
digraph Dependencies {
rankdir = LR
node [shape=box]
"@
function Resolve-ApplicationName($appCIID)
{
# Check if we have already resolved this appCIID
$appname = $null
$appname = $CI_NameTable.Item($appCIID)
if ($appname -eq $null)
{
# Name not resolved get name from WMI
gwmi -ComputerName $server -Namespace "root\sms\site_$sitecode" -Class SMS_ApplicationLatest -Filter "CI_ID = '$appCIID'" | select LocalizedDisplayName | %{
$CI_NameTable.Add($appCIID, $_.LocalizedDisplayName)
return $_.LocalizedDisplayName
}
} else {
# Name already resolved
return $appname
}
}
function Get-Dependency($appCIID, $parentName, $depth)
{
$apprelation = $null
## Check if relation is already resolved and in cache
$apprelation = $AppDependenceTable | ?{$_.FromApplicationCIID -eq $appCIID}
## Generate Indent based on depth
$indent = ""
$i=0
for ($i=1;$i -le $depth;$i++)
{
$indent = "$indent "
}
if ($apprelation -eq $null)
{
## Relations not found in cache – need to query server WMI
$apprelation = gwmi -ComputerName $server -Namespace "root\sms\site_$sitecode" -class SMS_AppDependenceRelation -Filter "FromApplicationCIID='$appCIID'"
$apprelation | Add-Member -MemberType NoteProperty -Name WhereFrom -Value WMI
} else {
#$apprelation | Add-Member -MemberType NoteProperty -Name WhereFrom -Value Cache
return
}
$apprelation | %{
$ToApplicationCIID = $_.ToApplicationCIID
$ToApplicationName = Resolve-ApplicationName $ToApplicationCIID
$colourName = $null
$colourName = $levelColors.Item($depth)
if ($colourName -eq $null)
{
# deep node – no colour match, use gray
$colourName = "gray34"
}
$nextdepth = $depth+1
## Write Output
write-host "$indent -> $($ToApplicationName) (Depth : $depth; Query : $($_.WhereFrom); Parent : $parentName)"
## Adding to global output
$global:output += "`n`"$parentName`" -> `"$ToApplicationName`"[color=`"$colourName`", label=`"$depth`"]"
## Cache relation data
$AppDependenceTable.Add([pscustomobject]@{FromApplicationCIID=$_.FromApplicationCIID;ToApplicationCIID=$_.ToApplicationCIID})
## Recurse!
Get-Dependency $ToApplicationCIID $ToApplicationName $nextdepth
}
}
# Query WMI for the names of the main app to choose from
$appnames = gwmi -ComputerName $server -Namespace "root\sms\site_$sitecode" -query "select LocalizedDisplayName, LocalizedDescription, CI_ID, CI_UniqueID, CIVersion, CIType_ID from SMS_ApplicationLatest where LocalizedDisplayName like '%$query%'" | select-object -Property LocalizedDisplayName, LocalizedDescription, CI_ID, CI_UniqueID, CIVersion, CIType_ID
## Show Window of found applications from the query
$chosenapp = $appnames | Out-GridView -PassThru -Title "Choose Application"
## Quit Script if no app chosen
if (IsNull($chosenapp)) {
"No App Chosen"
exit
}
"Loading Dependency Tree for $($chosenapp.LocalizedDisplayName)…"
## Main Recursing Function
get-dependency $chosenapp.CI_ID $chosenapp.LocalizedDisplayName 1
$output += @"
}
"@
## Generate code filename from the app name
$code_filename = $chosenapp.LocalizedDisplayName.replace(' ','_')
$code_filepath = "$basepath\$code_filename.gv"
## Generate graph filename from the app name
$graph_filename = $chosenapp.LocalizedDisplayName.replace(' ','_')
$graph_filepath = "$basepath\$graph_filename.$graph_filetype"
## Save output as file
$output | out-file $code_filepath
## Make Output PNG
$output | & 'dot.exe' -T $graph_filetype -o $graph_filepath
ii $graph_filepath

 

Filter SMSTS.logs to view only the main action steps (using cmtrace)

If you have a long Task Sequence with lots of steps and want to pinpoint when specific actions ran, you can filter the log using Entry Text contains “The action (” in cmtrace.

If some steps are missing you may need to merge in the rolled over logs which are named smsts-xxxxx.log

SMSTS Filter

List Preparation for SQL Query

I often need to query a database using a where in filter and a list of items. The items are generally in a list format in an excel or csv file, once copied they are on different lines. This is how I convert to a usable string for an SQL query filter using Notepad++ :

  • Paste list into Notepad++
  • Open the Find and Replace dialog (Ctrl+F)
  • Change search mode to ‘Extended’
  • Find what : \r\n
  • Replace with : ‘,’
  • Add an quote mark at beginning and end of line
Screenshot of Find and Replace dialog in Notepad++

Then you can paste this string into a SQL ‘where in’ clause between the brackets.

Checking SCCM Task Sequence Variables using Powershell

While running an OSD task sequence you can use powershell to check the TS Environment variables.

I added a ‘Pause’ step using this guide. If, like me, you are using an x64 PE image, you need use the ‘serviceui.exe’ from either the Program Files location on the machine with MDT installed, or a MDT package :

%MDTInstallLocation%\Templates\Distribution\Tools\x64\
%MDTPackage%\Tools\x64\

Once you have your Task Sequence paused, you can press F8 (if enabled in the boot image) and start powershell.

$env = New-Object -COMObject Microsoft.SMS.TSEnvironment

This will create an interface to the com object we need to use to interact with the TS environment.

$env.GetVariables() | % {$_ + "=" + $env.Value($_)}

This will dump out all the variables to the screen, this is a bit messy as there are lots of variables – some with quite long xml text as the values.

This will output all the variables to a text file :

 
$env.GetVariables() | % {$_ + "=" + $env.Value($_)} | out-file "variables.txt"

This will display the value of a specific variable (e.g. Architecture):

 
$env.Value("Architecture")

You could adapt this into a script to run automatically, instead of manually if required.

SMBIOS Chassis Type Table

Table I use for reporting on ratio of Desktop / Laptops using Chassis Types. Derived from the DMTF SMBIOS Spec November 2016 : http://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.0.pdf

Hex Value Dec Value Meaning Laptop-Desktop-Other
1 1 Other Other
2 2 Unknown Other
3 3 Desktop Desktop
4 4 Low Profile Desktop Desktop
5 5 Pizza Box Desktop
6 6 Mini Tower Desktop
7 7 Tower Desktop
8 8 Portable Laptop
9 9 Laptop Laptop
A 10 Notebook Laptop
B 11 Hand Held Laptop
C 12 Docking Station Other
D 13 All In One Desktop
E 14 Sub Notebook Laptop
F 15 Space-saving Desktop
10 16 Lunch Box Desktop
11 17 Main Server Chassis Other
12 18 Expansion Chassis Other
13 19 SubChassis Other
14 20 Bus Expansion Chassis Other
15 21 Peripheral Chassis Other
16 22 RAID Chassis Other
17 23 Rack Mount Chassis Other
18 24 Sealed-case PC Desktop
19 25 Multi-system chassis Other
1A 26 Compact PCI Other
1B 27 Advanced TCA Other
1C 28 Blade Other
1D 29 Blade Enclosure Other
1E 30 Tablet Laptop
1F 31 Convertible Laptop
20 32 Detachable Laptop
21 33 IoT Gateway Other
22 34 Embedded PC Desktop
23 35 Mini PC Desktop
24 36 Stick PC Desktop

Design a site like this with WordPress.com
Get started