Friday 17 April 2015

Using SCCM to fix Unquoted Service Path Security issue

How to use Compliance Items to fix this issue (see this post http://www.commonexploits.com/unquoted-service-paths/ for a description of the security issue)

Via the main SCCM Management Console 

  • Click on Assets and Compliance Section 
  • Click on Compliance Settings, Configuration Items 
  • Click on Create Configuration Item ( I won't detail all the screens here - skipping to the "Settings" section - specify name, description, supported platforms etc as you see fit. 
  • You want to create a new Settings with a Setting Type of Script and a Data type of String.



Now we'll specify the Discovery Script and the Remediation Script

Discovery Script (Script Language : Powershell)






$Services = Get-WmiObject -Class Win32_Service

$Compliant = ""

$Services | % {
    #Check the path of each service, locate .exe in the path string, then check if any spaces in the path
    #Also check if any " in the path before the EXE.  If no " and a space exists, then its not compliant

    $ServiceName = $_.Name
    $ServiceEXEPath = $_.PathName

    #$RegKey = Get-ChildItem "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName"
    $RegPath = (Get-itemproperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName").ImagePath

    $IndexOfSpace = $RegPath.IndexOf(" ")
    $IndexOfEXE   = $RegPath.IndexOf(".exe")
    $IndexOfQuote = $RegPath.IndexOf("`"")

    if (($IndexOfSpace -ne -1) -and ($IndexOfSpace -lt $IndexOfEXE) -and ($IndexOfQuote -ne 0))
    {
        $newpath = "`"" + $Regpath.substring(0,$IndexofEXE+4) + "`""

        #Write-Host "[$ServiceName][$ServiceEXEPath][$RegPath][$IndexOfSpace][$IndexOfEXE][$IndexOfQuote][$NewPath]"
        $Compliant += "[$ServiceName][$ServiceEXEPath]`n"
    }
}

if ($Compliant.length -eq 0)
{
    $Compliant = "Compliant"
}

$Compliant



Remediation Script (Script Language : Powershell)

$Services = Get-WmiObject -Class Win32_Service

$Services | % {
    #Check the path of each service, locate .exe in the path string, then check if any spaces in the path
    #Also check if any " in the path before the EXE.  If no " and a space exists, then its not compliant

    $ServiceName = $_.Name
    $ServiceEXEPath = $_.PathName

    #$RegKey = Get-ChildItem "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName"
    $RegPath = (Get-itemproperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName").ImagePath

    $IndexOfSpace = $RegPath.IndexOf(" ")
    $IndexOfEXE   = ($RegPath.toupper()).IndexOf(".EXE")
    $IndexOfQuote = $RegPath.IndexOf("`"")

    if (($IndexOfSpace -ne -1) -and ($IndexOfSpace -lt $IndexOfEXE) -and ($IndexOfQuote -ne 0))
    {
        $newpath = "`"" + $Regpath.substring(0,$IndexofEXE+4) + "`""

        if (($RegPath.length) -gt ($IndexOfEXE+4))
        {
            $newpath += $RegPath.substring($IndexOfEXE+4,($Regpath.length - $IndexofEXE - 4))
        }

        #Write-Host "[$ServiceName]`n[$ServiceEXEPath]`n[$IndexOfSpace][$IndexOfEXE][$IndexOfQuote]`n[$NewPath]"

        Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName" -Name ImagePath -Value $newpath
   
    }
}

This should allow you to scan and check all the services across your entire estate for the unqouted service path vulnerability, and when you enable the Remediate option, automatically fix any misconfigured services.

Friday 14 February 2014

Custom WMI Classes and reporting into SCCM 2012

Based on my previous post re Custom WMI Classes (http://sccmshenanigans.blogspot.co.uk/2013/11/custom-wmi-classes-and-reporting-into.html) here's another custom class and scripts for obtaining Certificate information for all your devices.

Creating the new class

As with the previous example, run Powershell and run the command Get-WMIObject -class CertificateDetails and you should get an error re Invalid Class

So, same as with the MonitorDetails class, the code below will create the CertificateDetails custom WMI class

# Creates a new class in WMI to store our data
function Create-Wmi-Class()
{
    $newClass = New-Object System.Management.ManagementClass("root\cimv2", [String]::Empty, $null);

    $newClass["__CLASS"] = "CertificateDetails";

    $newClass.Qualifiers.Add("Static", $true)
    $newClass.Properties.Add("Thumbprint", [System.Management.CimType]::String, $false)
    $newClass.Properties["Thumbprint"].Qualifiers.Add("key", $true)
    $newClass.Properties["Thumbprint"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("SubjectName", [System.Management.CimType]::String, $false)
    $newClass.Properties["SubjectName"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("IssuerName", [System.Management.CimType]::String, $false)
    $newClass.Properties["IssuerName"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("Version", [System.Management.CimType]::String, $false)
    $newClass.Properties["Version"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("FriendlyName", [System.Management.CimType]::String, $false)
    $newClass.Properties["FriendlyName"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("ExpirationDate", [System.Management.CimType]::String, $false)
    $newClass.Properties["ExpirationDate"].Qualifiers.Add("read", $true)
    $newClass.Put()
}

# Check whether we already created our custom WMI class on this PC, if not, create it
[void](gwmi CertificateDetails -ErrorAction SilentlyContinue -ErrorVariable wmiclasserror)
if ($wmiclasserror) {
    try { Create-Wmi-Class }
    catch {
        "Could not create WMI class"
        Exit 1
    }
}

function Get-Cert()
{
    param ([string]$computer)

    $ro=[System.Security.Cryptography.X509Certificates.OpenFlags]"ReadOnly"
    $lm=[System.Security.Cryptography.X509Certificates.StoreLocation]"LocalMachine"
    $store=new-object System.Security.Cryptography.X509Certificates.X509Store("\\$computer\My",$lm)

    $store.Open($ro)

    $store.Certificates
}

$ComputerName = $ENV:COMPUTERNAME

$ComputerCerts = Get-Cert $ComputerName

$certs = @()

if ($ComputerCerts -ne $null)
{
    $ComputerCerts | % {
        $CertInfo = "" | select thumbprint,subjectname, issuername, version, friendlyname, expirationdate
        $CertInfo.Thumbprint = $_.thumbprint
        $CertInfo.SubjectName = $_.subjectname.name
        $CertInfo.issuername = $_.issuername.name
        $CertInfo.version = $_.version
        $CertInfo.friendlyname = $_.friendlyname.name
        $CertInfo.expirationdate = $_.getexpirationdatestring()
        $Certs += $CertInfo
    }
}

# Clear WMI
Get-WmiObject CertificateDetails | Remove-WmiObject

# And store the data in WMI
$Certs | % { $i=0 } {
    [void](Set-WmiInstance -Path \\.\root\cimv2:CertificateDetails -Arguments @{Thumbprint=$_.thumbprint;SubjectName=$_.SubjectName; IssuerName=$_.IssuerName; `
    Version=$_.Version; FriendlyName=$_.FriendlyName; ExpirationDate=$_.ExpirationDate})
    $i++
}


Once the above class has been created, retest on your machine and you should see information

Following the steps in the previous post, add the new class to the Hardware Inventory classes on Default Client Settings.  Again, watching the dataldr.log file you should see new SQL tables and views created.

When creating the baseline item, the discovery script is almost identical to the previous post: 

Discovery Script (Script Language : Powershell)
$ClassInfo = Get-WMIObject CertificateDetails -ErrorAction SilentlyContinue -ErrorVariable wmiclasserror

if ($wmiclasserror) { $ClassFound = 0 } else { $ClassFound = 1 }

$ClassFound

Create a new baseline item as per my previous posts, and then the certificate information will be reported back in you next hardware Inventory.

I also created 2 useful reports to use this information:

List of expired certificates

SELECT csys.name0, csys.Domain0, sys.Operating_System_Name_and0, sys.Active0, sys.Client0, certs.Thumbprint0,
    certs.FriendlyName0, certs.SubjectName0, CONVERT(DATE,certs.ExpirationDate0,104) AS ExpirationDate, certs.IssuerName0
FROM v_GS_COMPUTER_SYSTEM csys
LEFT OUTER JOIN v_R_System sys ON csys.ResourceID = sys.resourceID
INNER JOIN v_GS_CERTIFICATEDETAILS certs on csys.ResourceID = certs.ResourceID
WHERE CONVERT(DATE,certs.ExpirationDate0,104) < GETDATE()

List of certificates about to expire (within next 30 days)

SELECT csys.name0, csys.Domain0, sys.Operating_System_Name_and0, sys.Active0, sys.Client0, certs.Thumbprint0,
    certs.FriendlyName0, certs.SubjectName0, CONVERT(DATE,certs.ExpirationDate0,104) AS ExpirationDate, certs.IssuerName0
FROM v_GS_COMPUTER_SYSTEM csys
LEFT OUTER JOIN v_R_System sys ON csys.ResourceID = sys.resourceID
INNER JOIN v_GS_CERTIFICATEDETAILS certs on csys.ResourceID = certs.ResourceID
WHERE CONVERT(DATE,certs.ExpirationDate0,104) BETWEEN GETDATE() AND DATEADD(D,30,GETDATE())

Friday 29 November 2013

Custom WMI Class in SCCM 2012 for Hardware Warranty Information Part 2

In part 1 I covered how to create the new WMI class on each device and the powershell script used to interrogate the HP site and obtain warranty details and save to CSV files.

Part 2 here now covers using these CSV files and using a baseline item to read the CSV file and put the values back into the devices.

On the Primary Server I created a folder under INETPUB\WWWROOT called WarrantyDetails. Since IIS is always running on the CAS and Primary Servers, and to cater for any devices talking to SCCM, whether they are domain joined or DMZ/remote machines, we decided to use BITS for the transfer of the CSV file back from the SCCM servers to the individual devices.

So, as per Part 1, we want to create a new baseline item that gets the CSV file from the server, reads it, and fills in the blank WMI Class fields for Warranty Info and Warranty Date. Follow the instructions in Part for the creation of the baseline item. The detection and remediation scripts are below:


Discovery Script (Script Language : Powershell)

# If virtual then just say its compliant, there won't be any warranty details, otherwise if physical, force it to try warranty check

if ((Get-WmiObject Win32_ComputerSystem).model -match "VMWare")
{
    $WarrantyCheck = 1
}
else
{
    $WarrantyCheck = 0
}

$WarrantyCheck


Remediation Script (Script Language : Powershell)

$ComputerName = $ENV:COMPUTERNAME

Import-Module BITSTransfer
$WarrantyFile = $ComputerName + ".csv"
$SourceFile = "http://<NameofyourCASorPrimaryServer>/WarrantyInformation/$WarrantyFile"
$DestFile = "C:\Windows\temp\$WarrantyFile"

Start-BitsTransfer -source $SourceFile -destination $DestFile

if (get-childitem $DestFile)
{
    $WarrantyInfo = Import-Csv $DestFile

    Get-WmiObject WarrantyDetails | Remove-WmiObject

    [void](Set-WmiInstance -Path \\.\root\cimv2:WarrantyDetails -Arguments @{SerialNumber=$WarrantyInfo.SerialNumber;ProductNumber=$WarrantyInfo.ProductCode;WarrantyDate=$WarrantyInfo.WarrantyDate;WarrantyInfo=$WarrantyInfo.WarrantyInfo})
    Remove-Item $DestFile -force
}


And finally, we need to specify a Compliance Rule
  • Make sure the Rule Type is Value, and the the Value Equals 1
  •  Tick the box for Run the specified remediation script...and save

 And to test, run Powershell and run the command Get-WMIObject -class WarrantyDetail and you should now see the stored warranty information...ah, mine has expired :(



 

SCCM 2012 and returning Registry Keys to Hardware Inventory

If you've read my previous posts re custom WMI classes, you could create WMI classes via scripts and store any information you want in them, including registry entries.

If however you want to specifically return registry keys, then this is also possible.

What we'll end up with is this:


1st thing to do is create the new class. So, on your central CAS server (or your Primary server if you only have this) browse to your SCCM 2012 Installation Folder (usually C:\Program Files\Microsoft Configuration Manager\inboxes\clifiles.src\hinv. Locate the file called configuration.mof and edit this (flat text file)

Browse to the end of the file and add in the following:

#pragma namespace ("\\\\.\\root\\cimv2")
#pragma deleteclass("XXXRegistryValues", NOFAIL)
[DYNPROPS]
Class XXXRegistryValues
{
[key] string KeyName="";
String String;
Uint32 Integer;
};


So here we're creating the 3 fields that will show up in the HW inventory screen above, Keyname (string and the key), String (string field) for any text values and Integer (uint32 field) for any numerical data

Next we want to add 1 or more entries for each registry key you want to retrieve.

For example:

//Return the installed version of Powershell
[DYNPROPS]
Instance of XXXRegistryValues
{
    KeyName="PS_HKLM\\SOFTWARE\\Microsoft\\PowerShell\\1\\PowerShellEngine|PowerShellVersion";
    [PropertyContext("Local|HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\PowerShell\\1\\PowerShellEngine|PowerShellVersion")
    ,Dynamic,Provider("RegPropProv")] String;
};


//Return the registered WSUS server for the device
[DYNPROPS]
Instance of XXXRegistryValues
{
    KeyName="WSUS_HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\WindowsUpdate|WUServer";
    [PropertyContext("Local|HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows\\WindowsUpdate|WUServer")
    ,Dynamic,Provider("RegPropProv")] String;
};

and so on... add as many as you need.

Before you save the file, load up CMTRACE and watch the log file
<SCCMInstallFolder>\Logs\dataldr.log and you'll see the configuration.mof file be processed once its saved. And again, as with the custom WMI classes, you'll see new tables appear in the SQL database, in the case above dbo.XXXRegistryValues_DATA and dbo.XXXRegistryValues_HIST, as well as views referencing these tables.

We now need to create a MOF file to import into SCCM to make it available via the Hardware Inventory.

Create a new text file and add the following:

#pragma namespace ("\\\\.\\root\\cimv2\\SMS")
#pragma deleteclass("RLGRegistryValues", NOFAIL)

[SMS_Report(FALSE),SMS_Group_Name("XXXRegistryValues"),SMS_Class_ID("XXXRegistryValues"),
SMS_Context_1("__ProviderArchitecture=32|uint32"),
SMS_Context_2("__RequiredArchitecture=true|boolean")]
Class XXXRegistryValues: SMS_Class_Template
{
[SMS_Report(FALSE),key] string KeyName;
[SMS_Report(FALSE)] string String;
[SMS_Report(FALSE)] Uint32 Integer;
};

Safe this as a file with a .MOF extension somewhere. We will now add the class to the Default Client Settings Hardware Classes - almost identical process to the one used for Custom WMI Classes.


Updating SCCM Client Base Classes for Hardware Inventory

Run the SCCM Management Console
  • Click on Administration Section
  • Click on Client Settings
  • Right click on Default Client Settings, then properties (Has to be the default one to select the new class). If you have additional client settings you can enable and disable the class for collection afterwards, but it must first be selected via the Default Client Settings
  • Click Hardware Inventory
  • Click Set Classes and you'll see the following screen


  • This time, click the Import button, and browse to the .MOF file we've just created above and click Open and you should see the results below


  • Click Import to complete
  • As a default the class won't be selected on the Hardware Inventory Classes screen. Either select the box to enable it for all devices (since we're editing the Default Client Settings) or OK out of this screen and edit your relevant Client Settings and enable as desired.
And thats it - the next time the Hardware Inventory runs, you'll start to see your Registry Keys returned and available via resource explorer (and more importantly they're all available for creating custom reports).

Thursday 28 November 2013

Powershell and SCCM 2012

It can be useful sometimes to kick off SCCM functions on remote desktops that may not be available through the SCCM Console. Here's a couple of examples.

Remotely Run a Baseline on a Device

$ComputerName = "A name of some device on your network"

#Retrieve all the baselines available on the device
$Baselines = Get-WmiObject -ComputerName $ComputerName -Namespace root\ccm\dcm -Class SMS_DesiredConfiguration

#Search these for a specific baseline name to obtain Baseline Name and Version

if ($Baselines)
{
    $BaselineName = $Baselines | Where-Object {$_.DisplayName -match "xxxxxxxx"} | Select-Object -ExpandProperty Name
    $BaselineVersion = $Baselines | Where-Object {$_.DisplayName -match "xxxxxxxx"} | Select-Object -ExpandProperty Version

    $MC = Get-WmiObject -List -Namespace root\ccm\dcm -ComputerName $ComputerName | Where-Object { $_.name -eq "SMS_DesiredConfiguration" }
 

    $R = $MC.TriggerEvaluation($BaselineName, $BaselineVersion, $true, $true)
    $R | Format-Table
}


Doing it all via WMI calls also allows you to specify Credentials on the Get-WMIObject if needed, and if you link this to a SQL query looking directly at your SCCM database (see post http://sccmshenanigans.blogspot.co.uk/2013/11/useful-sccm-sql-queries.html re SQL queries) then you can run a block refresh of Baselines across devices.

Remotely run Hardware Inventory

$ComputerName = "A name of some device on your network"

$MC = Get-WmiObject -List -Namespace root\ccm -ComputerName $ComputerName | Where-Object { $_.name -eq "SMS_Client" }

$SMSCli.TriggerSchedule("{00000000-0000-0000-0000-000000000001}")

As with the above example, if you link it to a SQL query you can bulk execute the HW Inventory scans.

Other schedules that you can kick of are:
NameSchedule ID
HW Inventory{00000000-0000-0000-0000-000000000001}
SW Inventory{00000000-0000-0000-0000-000000000002}
Discovery Data Record{00000000-0000-0000-0000-000000000003}
Machine Policy Retrieval & Evaluation{00000000-0000-0000-0000-000000000021}
File Collection{00000000-0000-0000-0000-000000000010}
SW Metering Usage Report{00000000-0000-0000-0000-000000000022}
Windows Installer Source List{00000000-0000-0000-0000-000000000032}
Software Updates Scan{00000000-0000-0000-0000-000000000113}
Software Updates Store{00000000-0000-0000-0000-000000000114}
Software Updates Deployment{00000000-0000-0000-0000-000000000108}

A definitive list can be found via the ConfigMgr SDK under the section titled TriggerSchedule Method in Class SMS_Client

Custom WMI Class in SCCM 2012 for Hardware Warranty Information Part 1

Based on my previous post re Custom WMI Classes (http://sccmshenanigans.blogspot.co.uk/2013/11/custom-wmi-classes-and-reporting-into.html) here's another custom class and scripts for obtaining hardware warranty information.

Creating the new class

As with the previous example, run Powershell and run the command Get-WMIObject -class WarrantyDetails and you should get an error re Invalid Class

So, same as with the MonitorDetails class, the code below will create the WarrantyDetails custom WMI class

function Create-Wmi-Class()
{
    $newClass = New-Object System.Management.ManagementClass("root\cimv2", [String]::Empty, $null);

    $newClass["__CLASS"] = "WarrantyDetails";

    $newClass.Qualifiers.Add("Static", $true)
    $newClass.Properties.Add("SerialNumber", [System.Management.CimType]::String, $false)
    $newClass.Properties["SerialNumber"].Qualifiers.Add("key", $true)
    $newClass.Properties["SerialNumber"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("ProductNumber", [System.Management.CimType]::String, $false)
    $newClass.Properties["ProductNumber"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("WarrantyInfo", [System.Management.CimType]::String, $false)
    $newClass.Properties["WarrantyInfo"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("WarrantyDate", [System.Management.CimType]::String, $false)
    $newClass.Properties["WarrantyDate"].Qualifiers.Add("read", $true)
    $newClass.Put()
}

# Check whether we already created our custom WMI class on this PC, if not, create it
[void](Get-WMIObject WarrantyDetails -ErrorAction SilentlyContinue -ErrorVariable wmiclasserror)
if ($wmiclasserror)
{
    try { Create-Wmi-Class }
    catch
    {
        "Could not create WMI class"
        Exit 1
    }
}

$sSerialNumber = ((Get-WmiObject -Class win32_bios).serialnumber).trim()
$sProductNumber = $null
$sProductNumber = (( Get-WmiObject -ComputerName $ComputerName -namespace "root\hp\InstrumentedBIOS" -Query "select * from HP_BIOSSetting" | Where-Object { ($_.Name -eq "SKU Number") -or ($_.Name -eq "Product Number") } ).Value).trim()

if ($sproductnumber -eq $null)
{
    $sProductNumber = (Get-ItemProperty HKLM:\\HARDWARE\Description\System\BIOS).SystemSKU
}

# Clear WMI
Get-WmiObject WarrantyDetails | Remove-WmiObject

# And store the data in WMI
[Void](Set-WmiInstance -Path \\.\root\cimv2:WarrantyDetails -Arguments @{SerialNumber=$sSerialNumber; ProductNumber=$sProductNumber})


You may need to customise the $sProductNumber information - the example above is specifically for HP kit.

Once the above class has been created, retest on your machine and you should see information



We'll come back to the blank WarrantyDate and WarrantyInfo later.

Following the steps in the previous post, add the new class to the Hardware Inventory classes on Default Client Settings.  Again, watching the dataldr.log file you should see new SQL tables and views created.

When creating the baseline item, the discovery script is almost identical to the previous post:


Discovery Script (Script Language : Powershell)

$ClassInfo = Get-WMIObject WarrantyDetails -ErrorAction SilentlyContinue -ErrorVariable wmiclasserror

if ($wmiclasserror) { $ClassFound = 0 } else { $ClassFound = 1 }

$ClassFound


Obtaining Warranty Information (currently HP only)

If you do a Google search for any combination of script, warranty information, powershell, vb etc you'll find loads of posts with examples.  For HP unfortunately, all the posts I found had links for warranty lookup that no longer worked, either due to HP removing the link or redesigning their websites. It proved to be quite difficult to track down a working weblink, but I finally managed to work one out for desktop machines at least (have yet to work out or find a direct link that can be used for server kit)

The script below is specifically for HP kit but other links may/will exist for other manufacturers. The script reads the SQL table directly from SCCM for all devices with missing warranty information, scrapes the webpage for the relevant information, then stores the returned information in CSV files for later use.

#Get the relevant records directly from SQL for any reported devices with no warranty information
$SQLSelect = "SELECT ass.ResourceID, ass.Name0, ass.Model0, ass.Domain0, ass.Manufacturer0, war.SerialNumber0,
war.ProductNumber0, war.WarrantyInfo0, war.WarrantyDate0
FROM v_GS_Computer_System ass
INNER JOIN v_R_System sys on ass.ResourceID=sys.resourceID
INNER JOIN v_GS_PC_BIOS bios ON ass.ResourceID = bios.ResourceID
INNER JOIN v_GS_WARRANTYDETAILS war ON ass.ResourceID = war.ResourceID
WHERE ass.Manufacturer0 NOT LIKE 'VMWare%' and war.WarrantyInfo0 is null
AND sys.client0=1 AND sys.obsolete0=0 AND sys.Active0=1"

#SQL Server connection information
$SQLServer = "xxxxxxxx" # Name of SCCM SQL server (usually the CAS, or primary if only 1 primary)
$SQLDatabase = "CM_xxx" # Name of database with site code
$SQLAuth = "uid=xxxxxxxx;pwd=xxxxxxxx" # A SQL account with at least R/O access



$SQLConnection = New-Object System.Data.SqlClient.SqlConnection
$SQLConnection.ConnectionString="Server=$SQLServer;Database=$SQLDatabase;$SQLAuth"
$SQLConnection.Open()

$SQLCmd = New-Object System.Data.SqlClient.SqlCommand
$SQLCmd.CommandText = $SQLSelect
$SQLCmd.Connection  = $SQLConnection

$SQLAdapter = New-Object System.Data.SqlClient.SqlDataAdapter $SQLCmd
$SQLDataset = New-Object System.Data.DataSet


$SQLAdapter.Fill($SQLDataset) | Out-Null

if ($SQLDataset -ne $null)
{
    $ComputerList = $SQLDataset.Tables[0]

    $RowCount = 1

    $ComputerList | % {
        $ComputerName = $_.Name0
        $sSerialNumber = $_.SerialNumber0
        $sProductNumber = $_.ProductNumber0
        $sManufacturer = $_.Manufacturer0

        $Rows = ([array]($ComputerList)).length

        Write-Host "Processing Row $RowCount of $Rows...Checking $ComputerName..." -NoNewline
        $RowCount++

        switch ($sManufacturer)
        {
            "HP" {
                $sWebLink = "http://h10025.www1.hp.com/ewfrf/wc/weResults?cc=us&dlc=en&lc=en&tmp_weCountry=gb&X-view=mobile&tmp_weCountry=gb&tmp_weSerial=$sSerialNumber&tmp_weProduct=$sProductNumber&product=&lc=en&dlc=en&cc=us&tmp_weDest=&tmp_track_link=ot_we%2Fsubmit%2Fen_us%2Fentitlement%2Floc%3A0"
            }
            "Hewlett-Packard" {
                $sWebLink = "http://h10025.www1.hp.com/ewfrf/wc/weResults?cc=us&dlc=en&lc=en&tmp_weCountry=gb&X-view=mobile&tmp_weCountry=gb&tmp_weSerial=$sSerialNumber&tmp_weProduct=$sProductNumber&product=&lc=en&dlc=en&cc=us&tmp_weDest=&tmp_track_link=ot_we/submit/en_us/entitlement/loc:0"
            }
        }

        try
        {
            $OutputFile = ".\$ComputerName.csv"

            if (Test-Path $OutputFile)
            {
                Write-Host "File already exists..."
            }
            else
            {
                $webClient = new-object System.Net.WebClient
                $output = ($webClient.DownloadString($sWebLink)).tolower()

                $WarrantyInfo = ($output.Substring($output.IndexOf("warranty status"),250))
                $WarrantyDate = ($output.Substring($output.IndexOf("warranty end date"),250))

                if ($WarrantyInfo -match "warranty has expired") { $Warranty = "Expired" }
                if ($WarrantyInfo -match "covered under warranty") { $Warranty = "Covered under warranty" }

                $WarrantyDateFound = [regex]::Match($WarrantyDate,"[0-9]{4}-[0-9]{2}-[0-9]{2}").Groups[0].value

                write-host "$ComputerName, Warranty [$Warranty], Warranty End Date [$WarrantyDateFound]" -NoNewline

                $ComputerInfo = "" | select ComputerName, SerialNumber, ProductCode, WarrantyInfo, WarrantyDate

                $ComputerInfo.ComputerName = $ComputerName
                $ComputerInfo.SerialNumber = $sSerialNumber
                $ComputerInfo.ProductCode = $sProductNumber
                $ComputerInfo.WarrantyInfo = $Warranty
                $ComputerInfo.WarrantyDate = $WarrantyDateFound

                if (Test-Path $OutputFile) { Remove-Item $OutputFile -Force }

                Write-Host "Writing file for $ComputerName..."

                $ComputerInfo | Export-Csv $OutputFile -NoClobber -NoTypeInformation
            }
        }
        catch
        {
        }
    }
}
The CSV files that are output contain warranty information for each device.


Other weblinks i've come across on google searches re other manufacturers (NOTE i've NOT tested these as I have none of these devices to test!  Feel free to try them out)

Dell : http://support.dell.com/support/topics/global.aspx/support/my_systems_info/details?servicetag=' + (Machine Serial Number)

IBM / Lenovo :  http://www-307.ibm.com/pc/support/site.wss/warrantyLookup.do?type=' + LEFT (Machine Model, 4) + '&serial=' + (Machine Serial Number) + '&country=897'

Acer : http://secure3.tx.acer.com/FindSystem/findsystem.aspx?Title=Information&sn=' + (Machine Serial Number)

I'll cover reading these files back and storing the information back on each device's custom class in a later post.

Custom WMI Classes and reporting into SCCM 2012

Using Powershell, its possible to create CUSTOM WMI Classes and then include these in the Hardware Inventory for all devices.   This example will cover obtaining monitor information for desktop machines.

Creating the new class

To start, run Powershell and run the command Get-WMIObject -class MonitorDetails and you should get the error:


The script below will create the class, and populate it with the monitor details for your machine
:

#
# This script reads the EDID information stored in the registry for the currently connected monitors
# and stores their most important pieces of identification (Name, Size, Serial Number etc) in WMI
# for later retrieval by SCCM
#

# Reads the 4 bytes following $index from $array then returns them as an integer interpreted in little endian
 

function Get-LittleEndianInt($array, $index)
{
    # Create a new temporary array to reverse the endianness in
    $temp = @(0) * 4
    [Array]::Copy($array, $index, $temp, 0, 4)
    [Array]::Reverse($temp)
   
    # Then convert the byte data to an integer
    [System.BitConverter]::ToInt32($temp, 0)
}

# Creates a new class in WMI to store our data
function Create-Wmi-Class()

{
    $newClass = New-Object System.Management.ManagementClass("root\cimv2", [String]::Empty, $null);

    $newClass["__CLASS"] = "MonitorDetails";

    $newClass.Qualifiers.Add("Static", $true)
    $newClass.Properties.Add("DeviceID", [System.Management.CimType]::String, $false)
    $newClass.Properties["DeviceID"].Qualifiers.Add("key", $true)
    $newClass.Properties["DeviceID"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("ManufacturingYear", [System.Management.CimType]::UInt32, $false)
    $newClass.Properties["ManufacturingYear"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("ManufacturingWeek", [System.Management.CimType]::UInt32, $false)
    $newClass.Properties["ManufacturingWeek"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("DiagonalSize", [System.Management.CimType]::UInt32, $false)
    $newClass.Properties["DiagonalSize"].Qualifiers.Add("read", $true)
    $newClass.Properties["DiagonalSize"].Qualifiers.Add("Description", "Diagonal size of the monitor in inches")
    $newClass.Properties.Add("Manufacturer", [System.Management.CimType]::String, $false)
    $newClass.Properties["Manufacturer"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("Name", [System.Management.CimType]::String, $false)
    $newClass.Properties["Name"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("SerialNumber", [System.Management.CimType]::String, $false)
    $newClass.Properties["SerialNumber"].Qualifiers.Add("read", $true)
    $newClass.Properties.Add("DeviceFound", [System.Management.CimType]::UInt32, $false)
    $newClass.Properties["DeviceFound"].Qualifiers.Add("read", $true)
    $newClass.Put()
}

# Check whether we already created our custom WMI class on this PC, if not, create it
[void](Get-WMIObject MonitorDetails -ErrorAction SilentlyContinue -ErrorVariable wmiclasserror)
if ($wmiclasserror)

{
    try { Create-Wmi-Class }
    catch

    {
        "Could not create WMI class"
        Exit 1
    }
}

# Iterate through the monitors in Device Manager
$monitorInfo = @()
Get-WMIObject Win32_PnPEntity -Filter "Service='monitor'" | % { $k=0 }

{
    $mi = @{}
    $mi.Caption = $_.Caption
    $mi.DeviceID = $_.DeviceID
    # Then look up its data in the registry
    $path = "HKLM:\SYSTEM\CurrentControlSet\Enum\" + $_.DeviceID + "\Device Parameters"
    $edid = (Get-ItemProperty $path EDID -ErrorAction SilentlyContinue).EDID

    # Some monitors, especially those attached to VMs either don't have a Device Parameters key or an EDID value. Skip these
    if ($edid -ne $null)

    {
        # Collect the information from the EDID array in a hashtable
        $mi.Manufacturer += [char](64 + [Int32]($edid[8] / 4))
        $mi.Manufacturer += [char](64 + [Int32]($edid[8] % 4) * 8 + [Int32]($edid[9] / 32))
        $mi.Manufacturer += [char](64 + [Int32]($edid[9] % 32))
        $mi.ManufacturingWeek = $edid[16]
        $mi.ManufacturingYear = $edid[17] + 1990
        $mi.DeviceFound = 1
        $mi.HorizontalSize = $edid[21]
        $mi.VerticalSize = $edid[22]
        $mi.DiagonalSize = [Math]::Round([Math]::Sqrt($mi.HorizontalSize*$mi.HorizontalSize + $mi.VerticalSize*$mi.VerticalSize) / 2.54)

        # Walk through the four descriptor fields
        for ($i = 54; $i -lt 109; $i += 18)

        {
            # Check if one of the descriptor fields is either the serial number or the monitor name
            # If yes, extract the 13 bytes that contain the text and append them into a string
            if ((Get-LittleEndianInt $edid $i) -eq 0xff)

            {
                for ($j = $i+5; $edid[$j] -ne 10 -and $j -lt $i+18; $j++) { $mi.SerialNumber += [char]$edid[$j] }
            }
            if ((Get-LittleEndianInt $edid $i) -eq 0xfc)

            {
                for ($j = $i+5; $edid[$j] -ne 10 -and $j -lt $i+18; $j++) { $mi.Name += [char]$edid[$j] }
            }
        }
       
        # If the horizontal size of this monitor is zero, it's a purely virtual one (i.e. RDP only) and shouldn't be stored
        if ($mi.HorizontalSize -ne 0)

        {
            $monitorInfo += $mi
        }
    }
   
}

$monitorInfo

# Clear WMI
Get-WmiObject MonitorDetails | Remove-WmiObject

# And store the data in WMI
$monitorInfo | % { $i=0 }

{
    [void](Set-WmiInstance -Path \\.\root\cimv2:MonitorDetails -Arguments @{DeviceID=$_.DeviceID; ManufacturingYear=$_.ManufacturingYear; `
    ManufacturingWeek=$_.ManufacturingWeek; DiagonalSize=$_.DiagonalSize; Manufacturer=$_.Manufacturer; Name=$_.Name; SerialNumber=$_.SerialNumber; DeviceFound=$_.DeviceFound})
    $i++
}


So now, running Get-WMIObject -class MonitorDetails should return some information (2 monitors shown as attached to this device)



Now we need to get the newly created class into SCCM. First of all, we'll add it to the inventoried class list.

Updating SCCM Client Base Classes for Hardware Inventory

Run the SCCM Management Console
  • Click on Administration Section
  • Click on Client Settings
  • Right click on Default Client Settings, then properties (Has to be the default one to select the new class). If you have additional client settings you can enable and disable the class for collection afterwards, but it must first be selected via the Default Client Settings
  • Click Hardware Inventory
  • Click Set Classes and you'll see the following screen


  • Click the Add Button, then click Connect

  • Specify the machine name where the class was created above then click Connect
  • Locate the newly created class MonitorDetails, tick the box, then click OK



When you get returned to the Hardware Inventory Class screen, the new class MonitorDetails will be listed and will be selected. If you have other custom Client Settings, deselect the new class at this point otherwise it will be picked up for all devices that the Default Client Settings are assigned to.



If you have other custom Client Settings, go into each relevant one, and under Hardware Inventory, Set Classes, and then tick the box for MonitorDetails.

Before you click OK to save your settings, its a good idea at this point to load up CMTRACE and load up the log file <SCCMInstallFolder>\Logs\dataldr.log - you will see entries go through for the new class as its added to the SCCM database as a new table (in this case you'll see 2 new tables created in SQL - dbo.MONITORDETAILS_DATA and dbo.MONITORDETAILS_HIST), you will also see SQL views created (dbo.v_GS_MONITORDETAILS)

So now the new class has been selected, and the relevant SQL structures updated - we now need to tell our client devices to use and create the new WMI class and report information back to SCCM.

Client Baseline Settings

Back to the main SCCM Management Console
  • Click on Assets and Compliance Section
  • Click on Compliance Settings, Configuration Items
  • Click on Create Configuration Item ( I won't detail all the screens here - skipping to the "Settings" section - specify name, description, supported platforms etc as you see fit.
  • You want to create a new Settings with a Setting Type of Script and a Data type of String. Now we'll specify the Discovery Script and the Remediation Script



Discovery Script (Script Language : Powershell)

$ClassInfo = Get-WMIObject MonitorDetails -ErrorAction SilentlyContinue -ErrorVariable wmiclasserror

if ($wmiclasserror) { $ClassFound = 0 } else { $ClassFound = 1 }

$ClassFound


Remediation Script (Script Language : Powershell)

The Remediation script is the Powershell script at the top of this post - just copy and paste it in...

At this point we now need to specify a Compliance Rule
  • Make sure the Rule Type is Value, and the the Value Equals 1
  • Tick the box for Run the specified remediation script...and save


  • Once the configuration item is created, create a Configuration Baselines and add the new item above.
  • And finally, Deploy the Configuration Baseline to a collection - at this point don't forget to tick the box Remediate noncompliant rules when supported, otherwise the new class won't be created on your devices!

Now thats all done, when you look at the hardware inventory for any device, the monitor details will now be listed (Devices, right click, Start, Resource Explorer).



And finally, run a report to retrieve your new custom class!

SELECT * FROM v_GS_MONITORDETAILS