Blog Post

Core Infrastructure and Security Blog
5 MIN READ

Extending Hardware Inventory for System Center Configuration Manager

BrandonWilson's avatar
BrandonWilson
Icon for Microsoft rankMicrosoft
May 15, 2019

First published on TECHNET on Oct 15, 2018

Hello everyone, Jonathan Warnken here, and I am a Premier Field Engineer (PFE) for Microsoft. I primarily support Configuration Manager and I have been getting a lot of questions recently on how to collect custom information and include it in the device inventory within Configuration Manager. I wanted to share one way to accomplish this that demonstrates some of the great ways to extend the built-in features. For this post, I am going to show how to capture the information about local machine certificates. I do want to take a moment to thank MVP Sherry Kissinger for this post with the base powershell script used to collect the certificate information.

#Disclaimer
The sample scripts are not supported under any Microsoft standard support program or service. The sample scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages.#

Now on to the good stuff. PowerShell makes it easy to get information about certificates. Using get-childitem and selecting one certificate we can see all the information available



While you can collect all of this information we are going to limit this down to just the Thumbprint, Subject, Issuer, NotBefore, NotAfter, and FriendlyName. We are also going to add a custom value of ExpiresinDays and ScriptLastRan. Next, we use a PowerShell script to collect the information and publish it to a custom wmi class.

https://github.com/mrbodean/AskPFE/blob/master/ConfigMgr%20Certificate%20Inventory/publish-CertInfo2WMI.ps1

Next create a configuration item that uses the script to publish the certificates in the local machine personal store, the local machine trusted publishers, and the local machine trusted root certificate stores to wmi that will allow the hardware inventory to collect the information.

    1. Download https://github.com/mrbodean/AskPFE/raw/master/ConfigMgr%20Certificate%20Inventory/Inventory%20Machine%20Certificates.cab to c:\temp\Examples\
    2. Navigate to \Assets and Compliance\Overview\Compliance Settings\Configuration Baselines
    3. Click on "Import Configuration Data" (You will find this as a button on the top toolbar or in the context menu when you right click on Configuration Baselines
    4. Select C:\temp\Examples\Inventory Machine Certificates.cab
    5. Click Yes on the warning "The publisher of Inventory Machine Certificates.cab file could not be verified. Are you sure that you want to import this file?"
    6. Click next twice to progress through the wizard and once complete, click close.
    7. You will now see a new sub folder named Custom under Configuration Items (\Assets and Compliance\Overview\Compliance Settings\Configuration Items\Custom) and a configuration item named "Inventory Machine Certificates" in the Custom folder.
    8. You will also see a Configuration baseline named "Inventory Machine Certificates"
    9. Deploy this baseline to a test collection


The documentation for using configuration items is available at:

https://docs.microsoft.com/en-us/sccm/compliance/deploy-use/configuration-items-for-devices-managed-with-the-client

https://docs.microsoft.com/en-us/sccm/compliance/deploy-use/create-configuration-baselines

https://docs.microsoft.com/en-us/sccm/compliance/deploy-use/deploy-configuration-baselines

https://docs.microsoft.com/en-us/sccm/compliance/deploy-use/monitor-compliance-settings

These steps will extend the Hardware Inventory to collect the certificate information that has been published in WMI. To extend the inventory you must use a MOF file, MOF files are a convenient way to change WMI settings and to transfer WMI objects between computers. For more info see https://technet.microsoft.com/en-us/library/cc180827.aspx

    1. Download https://raw.githubusercontent.com/mrbodean/AskPFE/master/ConfigMgr%20Certificate%20Inventory/CertInfo.mof to c:\temp\Examples\
    2. Create a new Custom Device Client Setting (\Administration\Overview\Client Settings)
    3. Name the setting "Custom HW Inventory" and only enable Hardware Inventory
    4. Select Hardware Inventory on the left just under General
    5. Ensure Enable hardware inventory on clients is set to yes
    6. The default schedule is for 7 days, update the schedule if you would like to change it
    7. Click the "Set Classes …" button
    8. Click on the "Import …" button
    9. Select the c:\temp\Examples\CertInfo.mof
    10. Once back on the Hardware Inventory Classes dialog ensure the CertInfo (cm_CertInfo) class is enabled
    11. Click Ok
    12. Click Ok (again)
    13. Deploy the "Custom HW Inventory" Client Setting to a test collection.


Once the configuration item runs and publishes the data info WMI, the next time hardware inventory runs for systems in the test collection the certificate information will be available for reporting in Configuration Manager.

These steps will create console query that you can use to search for systems with a specific certificate thumbprint

    1. Download https://raw.githubusercontent.com/mrbodean/AskPFE/master/ConfigMgr%20Certificate%20Inventory/Find_Cert_Query.MOF to c:\temp\Examples\
    2. Navigate to \Monitoring\Overview\Queries
    3. Click on "Import Objects", this is available a button on the top toolbar and the context menu when you right click on Queries
    4. Click next to navigate through the wizard
    5. On the MOF File Name step, select c:\temp\Examples\Find_Cert_Query.MOF
    6. Once the import completes, you will see a query named "Find Machines with a Certificate by thumbprint"
    7. Once you have systems reporting the certificates as part of the inventory you can run this report
    8. When you run this report, it will prompt you for the thumbprint of a certificate to search for
    9. If any systems are found with the certificate the system name and the thumbprint will be returned by the query

 

This is a SQL query that can be used to view the certificate inventory data and can also be used as the basis for creating a custom report

Select sys.Name0 as 'Name', Location0 as 'Certificate Location', FriendlyName0 as 'Friendly Name', ExpiresinDays0 as 'Expires in Days', Issuer0 as Issuer, NotAfter0 as 'Not After', NotBefore0 as
'Not Before', Subject0 as Subject, Thumbprint0 as Thumbprint, ScriptLastRan0 as 'Script last Ran'

 

from v_GS_CM_CERTINFO

 

Inner Join v_R_System as sys ON v_GS_CM_CERTINFO.ResourceID = sys.ResourceID




Thank you for reading, and I hope this helps you out!

Updated Aug 28, 2019
Version 5.0
  • I would like to contribute by adding the certificate model output to the script.

     

    ## Delete any existing instances
    $CurrentEA = $ErrorActionPreference
    $ErrorActionPreference = 'SilentlyContinue'
    (Get-WmiObject -Namespace root\cimv2 -class cm_CertInfo).Delete()
    $ErrorActionPreference = $CurrentEA
    
    ## Create Class if it doesn't exist in root\cimv2
    $newClass = New-Object System.Management.ManagementClass ("root\cimv2", [String]::Empty, $null);
    $newClass["__CLASS"] = "cm_CertInfo";
    $newClass.Qualifiers.Add("Static", $true)
    $newClass.Properties.Add("Location", [System.Management.CimType]::String, $false)
    $newClass.Properties["Location"].Qualifiers.Add("Key", $true)
    $newClass.Properties.Add("Handle", [System.Management.CimType]::String, $false)
    $newClass.Properties["Handle"].Qualifiers.Add("Key", $true)
    $newClass.Properties.Add("Thumbprint", [System.Management.CimType]::String, $false)
    $newClass.Properties.Add("Subject", [System.Management.CimType]::String, $false)
    $newClass.Properties.Add("Issuer", [System.Management.CimType]::String, $false)
    $newClass.Properties.Add("NotBefore", [System.Management.CimType]::String, $false)
    $newClass.Properties.Add("NotAfter", [System.Management.CimType]::String, $false)
    $newClass.Properties.Add("FriendlyName", [System.Management.CimType]::String, $false)
    $newClass.Properties.Add("ExpiresinDays", [System.Management.CimType]::uint32, $false)
    $newClass.Properties.Add("Template", [System.Management.CimType]::String, $false)
    $newClass.Properties.Add("ScriptLastRan", [System.Management.CimType]::String, $false)
    $newClass.Put()|Out-Null
    
    function Publish-Certs2WMI{
        [CmdletBinding()]
        Param
        (
        # Path to Cert store to Publish
        [Parameter(Mandatory=$true,
        ValueFromPipelineByPropertyName=$true
        )]
        [ValidateScript({
            If(Test-Path "Cert:\$_"){$true}
            else{
                throw "Unable to access Certificates in $_ !!"
            }
        })]
        $Path
        )
        foreach ($Cert in (Get-ChildItem -Recurse cert:\$Path)){
            # Expires in...
                $ExpiresInDays = ($cert.NotAfter-(Get-Date)).TotalDays
                $ExpiresInDays = [int]$ExpiresInDays
                if ($ExpiresInDays -lt 0){
                    $ExpiresInDays = 0
                }
                $Today = (Get-Date)
                $templateExt = $Cert.Extensions | Where-Object{ (($_.Oid.FriendlyName -eq 'Informações de Modelo de Certificado') -or ($_.Oid.FriendlyName -eq 'Certificate Template Information')) } | Select-Object -First 1
    
                if($templateExt) {
                    $information = $templateExt.Format(1)
                
                    # Extract just the template name in $Matches[1]
                    if($information -match "^Template=(.+)\([0-9\.]+\)") {
                        set-wmiinstance -Namespace root\cimv2 -class cm_CertInfo -ErrorAction SilentlyContinue -argument @{
                            Location=$Path
                            Handle=$cert.Handle
                            Subject=$cert.subject
                            Issuer=$cert.Issuer
                            Thumbprint=$cert.thumbprint
                            NotBefore=$cert.NotBefore
                            NotAfter=$cert.NotAfter
                            FriendlyName=$cert.Friendlyname
                            ExpiresInDays=$ExpiresInDays
                            Template=$Matches[1]
                            ScriptLastRan=$Today
                        }|Out-Null
                    } else {
                        # No regex match, just return the complete information then
                        set-wmiinstance -Namespace root\cimv2 -class cm_CertInfo -ErrorAction SilentlyContinue -argument @{
                            Location=$Path
                            Handle=$cert.Handle
                            Subject=$cert.subject
                            Issuer=$cert.Issuer
                            Thumbprint=$cert.thumbprint
                            NotBefore=$cert.NotBefore
                            NotAfter=$cert.NotAfter
                            FriendlyName=$cert.Friendlyname
                            ExpiresInDays=$ExpiresInDays
                            Template=$information
                            ScriptLastRan=$Today
                        }|Out-Null
                    }
                } else {
                    # No template name found
                        set-wmiinstance -Namespace root\cimv2 -class cm_CertInfo -ErrorAction SilentlyContinue -argument @{
                            Location=$Path
                            Handle=$cert.Handle
                            Subject=$cert.subject
                            Issuer=$cert.Issuer
                            Thumbprint=$cert.thumbprint
                            NotBefore=$cert.NotBefore
                            NotAfter=$cert.NotAfter
                            FriendlyName=$cert.Friendlyname
                            ExpiresInDays=$ExpiresInDays
                            Template=$null
                            ScriptLastRan=$Today
                        }|Out-Null
                }
        }
    }
    
    $CertPaths = 'LocalMachine\TrustedPublisher','LocalMachine\Root','LocalMachine\My'
    foreach($CertPath in $CertPaths){Publish-Certs2WMI -Path $CertPath}
    
    If(@(Get-WmiObject -Namespace root\cimv2 -class cm_CertInfo).count -gt 0){Write-Host "Compliant"}

     

  • Thank you all for your input and findings 🙂 Since the original content got a bit dated, I will touch base with the original author to get his take, and make updates to the content as necessary.

  • patman42's avatar
    patman42
    Copper Contributor

    Well, it was all going great - until I found that the powershell script writes the NotBefore, NotAfter and ScriptLastRan0 values into WMI as CimType 'String' rather than 'DateTime'. The result from our environment was a variety of US/UK date formats and 12hr/24hr times stored as text. 

    [BrandonWilson- no slight intended, this was still a great post and has already saved my bacon; just needed a little refinement for our use.]

     

    Anyway, I'm working on an update to the powershell to rewrite the dates as CimType 'DateTime'; in the meantime the dates can be 'standardised' in the report SQL instead, for example:

    SELECT
      v_GS_CM_CERTINFO.ExpiresinDays0
      ,v_R_System.Netbios_Name0
      ,v_R_System.Full_Domain_Name0
      ,v_GS_CM_CERTINFO.Issuer0
      ,v_GS_CM_CERTINFO.Subject0
      ,v_GS_CM_CERTINFO.FriendlyName0
      , case  right(v_GS_CM_CERTINFO.NotAfter0, 2) 
            when 'AM' then CONVERT(nvarchar, CONVERT(DATETIME, v_GS_CM_CERTINFO.NotAfter0, 101), 120)
            when 'PM' then CONVERT(nvarchar, CONVERT(DATETIME, v_GS_CM_CERTINFO.NotAfter0, 101), 120)
        else (substring(v_GS_CM_CERTINFO.NotAfter0, 7,4) + '-' + substring(v_GS_CM_CERTINFO.NotAfter0, 4,2) + '-' + left(v_GS_CM_CERTINFO.NotAfter0, 2)) + ' ' + right(v_GS_CM_CERTINFO.NotAfter0, 8) end as NotAfter0
      , case  right(v_GS_CM_CERTINFO.NotBefore0, 2)   
            when 'AM' then CONVERT(nvarchar, CONVERT(DATETIME, v_GS_CM_CERTINFO.NotBefore0, 101), 120)
            when 'PM' then CONVERT(nvarchar, CONVERT(DATETIME, v_GS_CM_CERTINFO.NotBefore0, 101), 120)
        else (substring(v_GS_CM_CERTINFO.NotBefore0, 7,4) + '-' + substring(v_GS_CM_CERTINFO.NotBefore0, 4,2) + '-' + left(v_GS_CM_CERTINFO.NotBefore0, 2)) + ' ' + right(v_GS_CM_CERTINFO.NotBefore0, 8) end as NotBefore0
      , case  right(v_GS_CM_CERTINFO.ScriptLastRan0, 2) 
            when 'AM' then CONVERT(nvarchar, CONVERT(DATETIME, v_GS_CM_CERTINFO.ScriptLastRan0, 101), 120)
            when 'PM' then CONVERT(nvarchar, CONVERT(DATETIME, v_GS_CM_CERTINFO.ScriptLastRan0, 101), 120)
        else (substring(v_GS_CM_CERTINFO.ScriptLastRan0, 7,4) + '-' + substring(v_GS_CM_CERTINFO.ScriptLastRan0, 4,2) + '-' + left(v_GS_CM_CERTINFO.ScriptLastRan0, 2)) + ' ' + right(v_GS_CM_CERTINFO.ScriptLastRan0, 8) end as ScriptLastRan0
    
    FROM
      v_GS_CM_CERTINFO
      INNER JOIN v_R_System ON v_GS_CM_CERTINFO.ResourceID = v_R_System.ResourceID

    This will output the dates as YYYY-MM-DD hh:mm:ss , for example 2020-02-20 12:34:56 (which is Excel-friendly, so will format nicely after exporting)

    Hope this helps!

  • patman42's avatar
    patman42
    Copper Contributor

    Likewise - very useful post, thanks! :happyface:

     

    Just one other thing; the "Publish Certificate Inventory" script has a slight flaw, due to the fact that powershell .Count - can't!

    The final line reads (expanded for clarity):

    If(  (Get-WmiObject -Namespace root\cimv2 -class cm_CertInfo).count -gt 0 ) {Write-Host "Compliant"}

     

    but .Count returns nothing if the actual number of objects is one, due to some kind of array issue, so @ is needed:

    If( @(Get-WmiObject -Namespace root\cimv2 -class cm_CertInfo).count -gt 0 ) {Write-Host "Compliant"}

    Then all is tickety-boo.

    Hope this helps!

     

  • MarcusHolland's avatar
    MarcusHolland
    Copper Contributor

    Thanks Brandon for the great article! Exactly what we were looking for 😉

     

    On the section importing certinfo.mof, it's worth noting that I was only able to do this within the default client settings hardware inventory. If attempting to import directly into the new custom HW inventory client settings, you receive the error below:  

    By importing certinfo.mof into the default client settings, you can import but then disable, thereby creating the inventory classes and settings but not using them. You can then enable the certinfo class in your custom HW inventory settings, as it already exists. Bingo!

     

    Thanks for the help!

     

    Kind regards,

    Marcus.