Mikael Blomqvist

ConfigMgr Client Registration Flow

When a client installation is completed it must register the device in order for the computer object to be created and approved in ConfigMgr. Until that validation has taken place the ConfigMgr client will only have two actions visible on the actions tab; Machine Policy Retrieval & Evaluation Cycle, User Policy Retrieval & Evaluation Cycle.

If the registration is not successful then there are a few log files both on the client side and on the server side which needs to be investigated, starting with the logs on the client and then move on to the Management Point logs and Site Server logs.

Client:

  1. Once ConfigMgr client is installed, ClientIDManagerStartup.log creates and maintains the client SMS GUID and identifies tasks performed during client registration and assignment.
  2. Client Registration request send to MP. You can find MP communication in ccmmessaging.log on client machine.

Server:

  1. MP_RegistrationManager (MP_RegistrationManager.log, in the folder ..\SMS_CCM\Logs on the Management Point server) process the registration request from client and complete the validation.
  2. MP_RegistrationManager writes the *.rdr file for client under “[ConfigMgr installDir]\inboxes\auth\ddm.box\regreq\” on the Site Server.
  3. SMS_Discovery_DATA_MANAGER (ddm.log) process the *.rdr file and update into Primary database.

This post is inspired by a longer blog post about client push installations by https://karthickjokirathinam.com/

Force Evaluation of a Baseline

This script will trigger a specified baseline to evaluate on a computer using the run script function in SCCM. If no baseline is specified all baselines on the computer will be evaluated.

param (
    [String][Parameter(Mandatory = $False, Position = 1)] $BLName
)

$ComputerName = $env:COMPUTERNAME

If ($BLName -eq $Null) {
    $Baselines = Get-WmiObject -ComputerName $ComputerName -Namespace root\ccm\dcm -Class SMS_DesiredConfiguration
}
Else {
    $Baselines = Get-WmiObject -ComputerName $ComputerName -Namespace root\ccm\dcm -Class SMS_DesiredConfiguration | Where-Object { $_.DisplayName -like $BLName }
}
 
$Baselines | % {
 
 ([wmiclass]"\\$ComputerName\root\ccm\dcm:SMS_DesiredConfiguration").TriggerEvaluation($_.Name, $_.Version) 
 
}

CMPivot Examples

CMPivot is available in the Configuration Manager console or as a standalone app which you can install from <site install path>\tools\CMPivot\CMPivot.msi.

CMPivot allows you to query data from devices in real-time. This can be very useful in scenarios where you need to find out how a certain configuration is or what the value of a setting is.

Here are a few examples which i often use and adjust from as a starting point.

Find Content in a File

FileContent('%windir%\\ccm\\logs\\locationservices.log') 
| where Content like '%Distribution Point%' 
| project Device, Content 

Find Value of a Registry Key

Registry('HKLM:/SOFTWARE/Microsoft/CCM/Ccm*') 
| where Property == 'ProvisioningMode' 

Find the Version of a File

File('C:\\program files\\Windows Defender\\msmpeng.exe') 
| project Device, Version 

Find Entries in Application EventLog Which Matches a Certain Source

EventLog('Application') 
| where Source like 'CTG-UpdateFrontEnd' 
| summarize dcount(Device) by Message 
| render barchart 

Find all devices where the Windows Defender service is stopped, Defender Install path is C:\Program Files 

Service | where (Name == 'WinDefend')  
| where (state == 'stopped')  
| join Registry('HKLM://SOFTWARE/Microsoft/Windows Defender')  
| where Property == 'InstallLocation' and Value like 'C:\Program Files%' 

Script to count computers in AD which have changed computer password within a certain period

This script will count the number of Windows computers (excluding servers) in Active Directory under the specified path which have changed computer password within the last 90 days.

When the script completes it will write the computer Count to the prompt and it will send an email with a csv file attached to the specified recipients.

Modify the following variables:

  • Smtpserver – Name of the smtpserver / relay
  • From – Mail address which should appear as sender (Could be the name of the server from which the mail is sent)
  • Recipient – Mail address of the recipient(s).
  • ComputerpasswordAgeDays – Number of days since the password was last changed.
  • ADSearchBase – OU to search for computers. DistinguishedName attribute from AD.
$m = Get-Module -List ActiveDirectory
if(!$m) {
    Write-Host "AD Powershell Module is not installed. Install it and run the script again."
    Exit
}

else{
    Import-Module ActiveDirectory
}


#Mail
$smtpServer = "ex01.domain.com"
$from = "MAN01@domain.com"
$recipient = "user@domain.com"

#Variables
[string]$SavePath = "$PSScriptRoot\reports"
[int]$ComputerPasswordAgeDays = 90
[string]$ADSearchBase = "DC=domain,DC=com"


#--------------------------------------
#Name: sendMail 
#Arguments: 2 Arguments:
#           1. Computer Count
#           2. File to be attached
#Return: -
#Description: Sends an E-mail to recipient defined in $recipient containing the csv file and computer count.
#--------------------------------------

function sendMail{

     $mailinfo_computer_count = $args[0]
     $file = $args[1]
  

    $body = "Computers which have changed computer password within the last $ComputerPasswordAgeDays days.</br></br>"
    $body += $mailinfo_computer_count

    $subject = "Antal computere: " + $mailinfo_computer_count
    
                 
     #Sending email 
     Send-MailMessage -SmtpServer $smtpServer -From $from -To $recipient -Subject $subject -Body $body -BodyAsHtml -Attachments $file
  
}


$Date = (Get-Date -format "MM-dd-yyyy")
IF ((test-path $SavePath) -eq $False) { md $SavePath }
$ExportFile = "$SavePath\Workstations-$Date.csv"
$ComputerStaleDate = (Get-Date).AddDays(-$ComputerPasswordAgeDays)
$Computers = Get-ADComputer -SearchBase $ADSearchBase -filter { (passwordLastSet -ge $ComputerStaleDate) -and (OperatingSystem -notlike "*Server*") -and (OperatingSystem -like "*Windows*") } -properties Name, DistinguishedName, OperatingSystem,OperatingSystemServicePack, passwordLastSet,LastLogonDate,Description
$ComputerTrimmed = $Computers.Count
write-output "Number of computers: " ($ComputerTrimmed) 

$Computers | export-csv $ExportFile -NoTypeInformation
sendMail $ComputerTrimmed $ExportFile

Script to extract all GPOs that a specific User, Group or Computer either have or doesnt have rights to

If you ever need to provide information about which GPO’s a specific User, Group or Computer have or doesnt have access to you can use this script.

The script will run through all GPO’s in the domain and check if a given User, Group or Computer either have or havent been delegated permissons. The script will produce a text file listing all GPO’s including detailed information about linkpaths, WMI Filters, Modify Dates, if its link or not and much more.

All you have to do is supply 3 parameters

  • TargetName – Name of the target (ie. Authenticated Users)
  • TargetType – Type of the target (Group, User or Computer)
  • hasPermissions – Boolean to specify if you want to find all the GPO’s that the target has permissions to (True) or does not have permissons to (False)

Example:

This will list all the GPO’s which the Group “Authenticated Users” doesnt have access to.

Get-GPOPermissionReport -TargetName “Authenticated Users” -TargetType Group -hasPermission -$False

<#
 
.SYNOPSIS
    Runs through all GPOs and checks if a given target has permissions and adds result to a text file. 
 
.DESCRIPTION
    This script will produce a text file containing all GPO details of the GPOs where the target either have or doesnt have permissions to. 
 
.PARAMETER TargetName
    Name of the target 
 
.PARAMETER TargetType
    Type of target (Group, User or Computer)

.PARAMETER hasPermissions
    Boolean to specify if we want to find all the GPO's that the target has permission to (True) or does not have permission to (False)
   
.EXAMPLE
    Get-GPOPermissionReport -TargetName "Authenticated Users" -TargetType Group -hasPermission $False
    Lists all the GPO's where Authenticated Users doesnt have access

.EXAMPLE
    Get-GPOPermissionReport -TargetName "Authenticated Users" -TargetType Group -hasPermission $True
    Lists all the GPO's where Authenticated Users have access
  
.NOTES
    Cmdlet name:      Get-GPOPermissionReport
    Author:           Mikael Blomqvist
    DateCreated:      2017-07-06
 
#>


[CmdletBinding(DefaultParameterSetName="String")] 
Param(
 [Parameter(Mandatory=$True, Position=0, HelpMessage="TargetName")] 
            [string]$TargetName,
 [Parameter(Mandatory=$True, Position=1, HelpMessage="TargetType(User/Computer/Group)")] 
            [string]$TargetType,
 [Parameter(Mandatory=$True, Position=2, HelpMessage="Find all GPO's where target has permission(True/False)")] 
            [boolean]$hasPermission
)


#Load GPO module
 Import-Module GroupPolicy
 
 $date = Get-Date -Format yyyyMMddhhmmss

 $file = "$PSScriptRoot\GPOPermissions_$date.csv"

 #Prepare the text file with the correct headings
 Add-Content $file "Name,LinksPath,WMI Filter,CreatedTime,ModifiedTime,CompVerDir,CompVerSys,UserVerDir,UserVerSys,CompEnabled,UserEnabled,SecurityFilter,GPO Enabled,Enforced"

#Get all GPOs in current domain
 $GPOs = Get-GPO -All

#Check we have GPOs
 if ($GPOs) 
 {
    foreach ($GPO in $GPOs) 
    {
        $GPOName = $GPO.DisplayName
        $GPOGuid = $GPO.id
        
        #Retrieve the permission for the specified target
        $GPPermissions = Get-GPPermissions -Guid $GPOGuid -TargetName $TargetName -TargetType $TargetType -ErrorAction SilentlyContinue

        #Check if target have permissions or not and set permission to either True(has permission) or False(no permission)
        If($GPPermissions -eq $null)
        { 
            $permission = $False
        }
        Else
        {
            $permission = $True
        }

        # if permissions matches what we look for either true or false
        if ($permission -eq $hasPermission) 
        { 
            [xml]$gpocontent =  Get-GPOReport -Guid $GPOGuid -ReportType xml
            $LinksPaths = $gpocontent.GPO.LinksTo
            $Wmi = Get-GPO -Guid $GPOGuid | Select-Object WmiFilter
 
            $CreatedTime = $gpocontent.GPO.CreatedTime
            $ModifiedTime = $gpocontent.GPO.ModifiedTime
            $CompVerDir = $gpocontent.GPO.Computer.VersionDirectory
            $CompVerSys = $gpocontent.GPO.Computer.VersionSysvol
            $CompEnabled = $gpocontent.GPO.Computer.Enabled
            $UserVerDir = $gpocontent.GPO.User.VersionDirectory
            $UserVerSys = $gpocontent.GPO.User.VersionSysvol
            $UserEnabled = $gpocontent.GPO.User.Enabled
            $SecurityFilter = ((Get-GPPermissions -Guid $GPOGuid -All | ?{$_.Permission -eq "GpoApply"}).Trustee | ?{$_.SidType -ne "Unknown"}).name -Join ','
        
            if($LinksPaths -ne $null)
            {
                foreach ($LinksPath in $LinksPaths)
                {
                    Add-Content $file "$GPOName,$($LinksPath.SOMPath),$(($wmi.WmiFilter).Name),$CreatedTime,$ModifiedTime,$CompVerDir,$CompVerSys,$UserVerDir,$UserVerSys,$CompEnabled,$UserEnabled,""$($SecurityFilter)"",$($LinksPath.Enabled),$($LinksPath.NoOverride)"
                }   
            }
            else
            {
                Add-Content $file "$GPOName,$($LinksPath.SOMPath),$(($wmi.WmiFilter).Name),$CreatedTime,$ModifiedTime,$CompVerDir,$CompVerSys,$UserVerDir,$UserVerSys,$CompEnabled,$UserEnabled,""$($SecurityFilter)"",$($LinksPath.Enabled),$($LinksPath.NoOverride)"
            } 
        }
    }
}

Script to Copy Machine Variables from one computer to another

I recently had to create several computers in Configuration Manager in order to pre-stage them for an OSD Deployment. As the OSD Deployment is dependent on the computers having certain machine variables assigned i had to add these for every computer too. I wanted all the new computers to have exactly the same variables as an existing computer. Instead of doing that manually for every computer i came up with this script which will do it automatically.

The script takes as input the Site Code, Source Computer and Target Computer.

Source Computer is just the name of the computer in Configuration Manager that has the variables you want to copy. Target Computer can be the name of one or more Target Computers either supplied as a string of computers or as a text file where each line hold a computer name.

In this example:

Copy-MachineVariables -Source IT-14 -Target IT-13, IT-15

All machine variables will be copied from computer IT-14 to computers IT-13 and IT-15

See more examples in the code below. Enjoy!

<#
  
.SYNOPSIS
    Copy machine variables from one Configuration Manager Computer Object to one or more computers.
 
.DESCRIPTION
    By giving the name of a Source computer all machine variables from that computer will be gathered and copied to one or more 
    of the target computers specified. Target Computers can be supplied either as a string or as seperate lines in a file. 
    Existing machine variables on the target computer(s) will as default not be removed as the new variables will just be appended.
    By setting Replace to $True exsisting variables on the target computer(s) will be removed.
 
.PARAMETER Source
    Name of the computer from which the machine variables should be copied
 
.PARAMETER Target
    Name of the computer(s) to which the machine variables should be copied

.PARAMETER TargetFile
    Path to file containing name of one or more computer(s) to which the machine variables should be copied
 
.PARAMETER Replace
    Decides if machine variables on the target computer(s) should be replaced. By default variables will be appended. 
    If this is set to True all exsisting machine variables on the target computer(s) are overwritten.
  
.EXAMPLE
    Copy-MachineVariables -Source IT-14 -Target IT-13
    Copy all machine variables on computer IT-14 and append them to computer IT-13

.EXAMPLE
    Copy-MachineVariables -Source IT-14 -Target IT-13, IT-15
    Copy all machine variables on computer IT-14 and append them to computers IT-13 and IT-15
 
.EXAMPLE
    Copy-MachineVariables -Source IT-14 -Target IT-13 -OverWrite $True
    Copy all machine variables on computer IT-14 and replace them with all exsiting variables on computer IT-13

.EXAMPLE
    Copy-MachineVariables -Source IT-14 -TargetFile C:\TargetComputers.txt"
    Copy all machine variables on computer IT-14 and append them with all exsiting variables on computers in C:\TargetComputers.txt
 
.NOTES
    Cmdlet name:      Copy-MachineVariables
    Author:           Mikael Blomqvist
    DateCreated:      2017-05-26
 
#>


[CmdletBinding(DefaultParameterSetName="String")] 
Param(
 [Parameter(Mandatory=$True, Position=0, HelpMessage="Supply Site Code")] 
            [string]$SiteCode,
 [Parameter(Mandatory=$True, Position=1, HelpMessage="Computer name(s) of source computer(s)")] 
            [string]$Source,
 [Parameter(Mandatory=$True, Position=2, ParameterSetName="String", HelpMessage="Computer name(s) of target computer(s)")] 
            [string[]]$Target,
 [Parameter(Mandatory=$True, Position=2, ParameterSetName="File", HelpMessage="File containing Computer name(s) of target computer(s)")] 
            [string]$TargetFile,
 [Parameter(Mandatory=$False, ParameterSetName="String",HelpMessage="Replace current machine variables on target computers - if false then variables will be appended")] 
 [Parameter(Mandatory=$False, ParameterSetName="File",HelpMessage="Replace current machine variables on target computers - if false then variables will be appended")] 
            [boolean]$Replace=$False
 
)



Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1" # Import the ConfigurationManager.psd1 module 
Set-Location $SiteCode':' # Set the current location to be the site code.

$provider = Get-PSDrive -PSProvider CMSITE
$siteServer = $provider.Root

#Getting Machine Variables from Source Computer
Write-Host "Gathering machine variables from $Source"
$objSourceSettings = Get-WmiObject -Namespace "Root\SMS\Site_$SiteCode" -Class "SMS_MachineSettings" -ComputerName $siteServer | Where-Object {$_.ResourceID -eq (Get-CMDevice -Name $Source).ResourceID}
$objSourceSettings.get()
$variablesSource = $objSourceSettings.MachineVariables

try{
    If($TargetFile -ne $null){
    #Reading all computers from file
    $Target = Get-Content($TargetFile)
    }
}
catch{
  #No file supplied
}
#Writing Machine Variables to Target Computers
Foreach($t in $Target){
   
    $objTargetSettings = Get-WmiObject -Namespace "Root\SMS\Site_$SiteCode" -Class "SMS_MachineSettings" -ComputerName $siteServer | Where-Object {$_.ResourceID -eq (Get-CMDevice -Name $t).ResourceID}
   

    If($Append -eq $False){
    Write-Host "Appending variables to $t"    
        $objTargetSettings.Get()
        $variablesTarget = $objTargetSettings.MachineVariables
    }
    Else{
    Write-Host "Overwriting existing variables to $t"

    }

    #$variablesTarget

    foreach($variable in $variablesSource)
    {
       $newVar = $([wmiclass]"\\$siteServer\Root\SMS\Site_$($SiteCode):SMS_MachineVariable").CreateInstance()
       $newVar.Name = $variable.Name
       $newVar.Value = $variable.Value
       [System.Management.ManagementBaseObject[]]$variablesTarget += ($newVar)
    }

    $objTargetSettings.MachineVariables = $variablesTarget
    $objTargetSettings.Put()
}

Write-Host "Done"

Script to create PC Environment Report

This Script generates a dynamic PC Environment Report. The report can include various information from Configuration Manager and Active Directory depending on your needs. There is an option to send this report as a mail to the specified recipient(s).

The information that can be collected from Configuration Manager is:

  • Patch Compliance overview showing a given past number of Software Update Groups (default 3 Groups)
  • Application categorization showing the number of applications that are either superseeded, without deployment or expired
  • Devices categorization showing the number of devices that are active, inactive, obsolete and without a configuration manager client
  • Computer reboot overview showing how many computers that have not been rebooted within a given time period (default 5 days)

The information that can be collected from Active Directory:

  • Computer password information – how many computers that have and have not changed their computer password within a given time period (default 90 days)
  • Operating system Overview – showing how many different Operating System versions that are present in the domain and how many there are of each
<#

.SYNOPSIS
    Returns a environment status report within the areas that have been selected as parameters running the report

.DESCRIPTION
    Queries the ConfigMgr to get information about applications, devices and software updates. Runs a query towards AD to find information about last computer password change.
    These information can be included or excluded using the parameters. All the information gathered from SCCM and AD will then ultamately be sent to the recipients specified in the mail section.

.PARAMETER runAppScan
    Scan SCCM for applications to find both used an unused applications

.PARAMETER runClientScan
    Scan SCCM Computer Clients to find out how many inactive clients there are

.PARAMETER runPatchCompScan
    SCCM 3 Month Patch Compliance

.PARAMETER runRebootCount
     Find computers that have not rebooted within the last X days

.PARAMETER runOSCount
    AD Count number of unique Operating Systems in OU specified in $ADSearchBase and  Count number of computers within different Operating Systems

.PARAMETER runComputerPasswordCount
    #Find info about when computers last changed Computer password

.PARAMETER runComputerPasswordCount
    #Create the report with everything included

.EXAMPLE
    PCManagementReport -runAppScan $true -runPatchCompScan $true
    Creates a report including SCCM Application Scan and Path Compliance Graph

.EXAMPLE
    PCManagementReport -runEverything $true
    Creates a report including everything

.NOTES
    Cmdlet name:      PCManagementReport
    Author:           Mikael Blomqvist
    DateCreated:      2017-04-19

#>


[CmdletBinding(SupportsShouldProcess = $True)]
param
(
    [Parameter(Mandatory = $False, HelpMessage = "Scan ConfigMgr Applications (True/False)")] 
    [boolean]$runAppScan = $false,
    [Parameter(Mandatory = $False, HelpMessage = "Scan ConfigMgr Devices (True/False)")] 
    [boolean]$runClientScan = $false,
    [Parameter(Mandatory = $False, HelpMessage = "Create Path Compliance Scan (True/False)")] 
    [boolean]$runPatchCompScan = $true,
    [Parameter(Mandatory = $False, HelpMessage = "Calculate Computer Reboot numbers (True/False)")]
    [boolean]$runRebootCount = $false,
    [Parameter(Mandatory = $False, HelpMessage = "Count number of unique Operating Systems and the number of Computers within each Operation Systems (True/False)")]  
    [boolean]$runOSCount = $false,
    [Parameter(Mandatory = $False, HelpMessage = "Calculate Computer password change numbers (True/False)")] 
    [boolean]$runComputerPasswordCount = $false,
    [Parameter(Mandatory = $False, HelpMessage = "Run everything (True/False)")]
    [boolean]$runEverything = $false
)


#Load the ActiveDirectory Module

$m = Get-Module -List ActiveDirectory
if (!$m) {
    Write-Host "AD Powershell Module is not installed can not continue."
    Exit
}

else {
    Import-Module ActiveDirectory
}

$CMSiteServerFQDN = 'siteserver.domain.com' #Configuration Manager Site Server FQDN

$CMSiteServer = $CMSiteServerFQDN.ToString().split(".") | Select -First 1

#Load Configuration Manager PowerShell Module

try {
    Import-Module "\\$CMSiteServer\d$\Program Files\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1" -ErrorAction Stop
}
catch {
    $_.Exception.Message
    $_.Exception.ItemName
    Write-Host "Cannot load the Configuration Manager PowerShell Module from the specified location"
    break #Terminate if the module cannot be loaded
}

###############################################################################
#
#Edit these to the customer specific values and $CMSiteServer above
#
###############################################################################

#Mail
$smtpServer = "smtprelay.domain.com"
$mailinfo_to = "recipient1@domain.com", "recipient2@domain.com" 
$mailinfo_cc = ""
$mailinfo_from = "$env:computername@domain.com"
$logo = "logo.gif"
$Customername = "My Company"
$sendMail = $true

#Active Directory
[string]$ADSearchBase = "OU=ComputersWin10,DC=domain,DC=com" #Win10
[int]$ComputerPasswordAgeDays = 90
[int]$RebootTresholdDays = 5

#SCCM
$Collection = "All Windows 10" #Collection in SCCM which should be used as basis for the device count
$patchNumberOfMonth = 3 #Number of month contained in the patch compliance overview

#Graphic
$CorporateColor = "#780000"
$BackGroundColor = "#505A55"

#Misc
$ReportTitle = "System Report"
$printToConsole = $true #Write Output to console

##############################################################################
#
#Variables which should not be edited
#
##############################################################################

$File_ExpSuperseededApps = "$PSScriptRoot\ExpiredSuperseededApps.csv"
$File_CompNotBooted = "$PSScriptRoot\NotRebootedComputers.csv"

[string]$Attachments = "$OutFile"

#Mail
$OutFile = "$PSScriptRoot\StatusReport.html"
$subject = "$Customername : Status Report"

#AD
$OUName = (Get-ADOrganizationalUnit -Filter * -Properties CanonicalName -SearchBase $ADSearchBase -SearchScope Base).Name

#SCCM
$SiteCode = Get-PSDrive -PSProvider CMSITE #Configuration Manager Site Code
$SCCMNameSpace = "root\sms\site_$SiteCode"


$head = @"
<style>
H1{color: #000000}
H2{color: #FFFFFF}
H3{color: #000000}
p{color: $CorporateColor; font-size:70%}
TABLE {margin-left:50px;background-color: #FFFFFF;}
TR {padding: 0px}
TH {padding: 5px;background-color: $CorporateColor}
TD {padding: 15px;text-align: Center}
BODY {font-family: verdana;background-color: $BackGroundColor}
</style>
<Title>$ReportTitle</Title>
"@


#Build ADSummary table
$tbl_ADSummary = New-Object System.Data.DataTable ADSummary
$col_ADHeading = New-Object System.Data.DataColumn ADHeading, ([string])
$col_ADData = New-Object System.Data.DataColumn ADData, ([string])
$tbl_ADSummary.columns.Add($col_ADHeading)
$tbl_ADSummary.columns.Add($col_ADData)


#Build AppsSummary table
$tbl_AppsSummary = New-Object System.Data.DataTable AppsSummary
$col_AppsHeading = New-Object System.Data.DataColumn AppsHeading, ([string])
$col_AppsData = New-Object System.Data.DataColumn AppsData, ([string])
$tbl_AppsSummary.columns.Add($col_AppsHeading)
$tbl_AppsSummary.columns.Add($col_AppsData)


#Build DeviceSummary table
$tbl_DeviceSummary = New-Object System.Data.DataTable DeviceSummary
$col_DeviceHeading = New-Object System.Data.DataColumn DeviceHeading, ([string])
$col_DeviceData = New-Object System.Data.DataColumn DeviceData, ([string])
$tbl_DeviceSummary.columns.Add($col_DeviceHeading)
$tbl_DeviceSummary.columns.Add($col_DeviceData)

#Build OS Table
$tbl_ostable = New-Object System.Data.DataTable OS
$Col_OSName = New-Object System.Data.DataColumn OSName, ([string])
$Col_OSCount = New-Object System.Data.DataColumn OSCount, ([string])
$tbl_ostable.Columns.Add($Col_OSName)
$tbl_ostable.Columns.Add($Col_OSCount)

#create patch table and columns
$tbl_Compliance = New-Object System.Data.DataTable Patches
$Col_month = New-Object System.Data.DataColumn Month, ([string])
$Col_compliant = New-Object System.Data.DataColumn Compliant, ([decimal])
$Col_noncompliant = New-Object System.Data.DataColumn NonCompliant, ([decimal])
$Col_unknown = New-Object System.Data.DataColumn Unknown, ([decimal])
$tbl_Compliance.columns.Add($Col_month)
$tbl_Compliance.columns.Add($Col_compliant)
$tbl_Compliance.columns.Add($Col_noncompliant)
$tbl_Compliance.columns.Add($Col_unknown)

#Build PieChart for Last Computer PW Change
$tbl_piechart = New-Object System.Data.DataTable PWChange
$Col_category = New-Object System.Data.DataColumn Category, ([string])
$Col_amount = New-Object System.Data.DataColumn Amount, ([string])
$tbl_piechart.columns.Add($Col_category)
$tbl_piechart.columns.Add($Col_amount)

#Function to add rows to table
Function AddTo-Table {
    $tableName = $args[0]
    $tableHeading = $args[1]
    $tableHeadingValue = $args[2]
    $tableData = $args[3]
    $tableDataValue = $args[4]

    $row = $tableName.NewRow(); $row.$tableHeading = $tableHeadingValue; $row.$tableData = $tableDataValue
    $tableName.Rows.Add($row)

    If ($printToConsole) { Write-Host $tableHeadingValue ": " $tableDataValue }
}

#Function to add attachments to Sting that can be added to the mail 
Function Add-Attachment {

    [string]$file = $args[0]

    $Attachments = $Attachments + ',' + $file

    return $Attachments
}

#Create a Pie Chart with the supplied information
Function CreateChart {

    $datasource = $args[0]
    $title = $args[1]
    $ChartType = $args[2]
    $xValue = $args[3]
    $yValue = $args[4]
    $imagePath = $args[5]
    $imagewidth = $args[6]
    $imageheight = $args[7]
    $displayedinLegend = $args[8]


    [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")
    $scriptpath = $PSScriptRoot
 
    # chart object
    $chart1 = New-object System.Windows.Forms.DataVisualization.Charting.Chart
    $chart1.Width = $imagewidth
    $chart1.Height = $imageheight
    $chart1.BackColor = [System.Drawing.Color]::White
 
    # title 
    [void]$chart1.Titles.Add("$title")
    $chart1.Titles[0].Font = "Verdana,14pt"
    $chart1.Titles[0].Alignment = "topCenter"

    $chart1.Titles[0].ForeColor = $CorporateColor
 
    # chart area 
    $chartarea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
    $chartarea.Name = "ChartArea1"
    $chartarea.AxisY.MajorGrid.Enabled = $false
    $chartarea.AxisX.MajorGrid.Enabled = $false
    $chart1.ChartAreas.Add($chartarea)
 
    # legend 
    $legend = New-Object system.Windows.Forms.DataVisualization.Charting.Legend
    $legend.name = "Legend1"
    $chart1.Legends.Add($legend)

    # data series
    [void]$chart1.Series.Add("PWWithin")
    $chart1.Series["PWWithin"].ChartType = "$ChartType"
    $chart1.Series["PWWithin"].BorderWidth = 10
    $chart1.Series["PWWithin"].IsVisibleInLegend = $displayedinLegend
    $chart1.Series["PWWithin"].chartarea = "ChartArea1"
    $chart1.Series["PWWithin"].IsValueShownAsLabel = $true
   

    $datasource | ForEach-Object { $chart1.Series["PWWithin"].Points.addxy( $_.$xValue , $_.$yValue) }
 
    # save chart
    $chart1.SaveImage($imagePath, "png")
}

#Function to Create a stacked patch compliance graph showing the past software update groups
Function CreateComplianceChart {

    $datasource = $args[0]

    $Pic_PatchComp = "PatchCompliance_Stacked100.png"
    $Path_PatchComp = "$PSScriptRoot\$Pic_PatchComp"


    [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")
    $scriptpath = $PSScriptRoot
 
    # chart object
    $chart1 = New-object System.Windows.Forms.DataVisualization.Charting.Chart
    $chart1.Width = 800
    $chart1.Height = 600
    $chart1.BackColor = [System.Drawing.Color]::White
 
    # title 
    [void]$chart1.Titles.Add("Past $patchNumberOfMonth Month Patch Compliance Development")
    $chart1.Titles[0].Font = "Verdana,20pt"
    $chart1.Titles[0].Alignment = "topCenter"

    $chart1.Titles[0].ForeColor = $CorporateColor
 
    # chart area 
    $chartarea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
    $chartarea.Name = "ChartArea1"
    $chartarea.AxisY.Title = "Compliance %"
    $chartarea.AxisX.Title = "Month"
    $chartarea.AxisY.Interval = 10
    $chartarea.AxisX.Interval = 1
    $chart1.ChartAreas.Add($chartarea)
 
    # legend 
    $legend = New-Object system.Windows.Forms.DataVisualization.Charting.Legend
    $legend.name = "Legend1"
    $chart1.Legends.Add($legend)
 

    # data series
    [void]$chart1.Series.Add("Compliant")
    $chart1.Series["Compliant"].ChartType = "StackedColumn100"
    $chart1.Series["Compliant"].BorderWidth = 10
    $chart1.Series["Compliant"].IsVisibleInLegend = $true
    $chart1.Series["Compliant"].chartarea = "ChartArea1"
    $chart1.Series["Compliant"].Legend = "Legend1"
    $chart1.Series["Compliant"].color = "#2ECC71"
    $chart1.Series["Compliant"].IsValueShownAsLabel = $true
    $datasource | ForEach-Object { $chart1.Series["Compliant"].Points.addxy( $_.Month , $_.Compliant) }
 

 
    # data series
    [void]$chart1.Series.Add("NonCompliant")
    $chart1.Series["NonCompliant"].ChartType = "StackedColumn100"
    $chart1.Series["NonCompliant"].IsVisibleInLegend = $true
    $chart1.Series["NonCompliant"].BorderWidth = 10
    $chart1.Series["NonCompliant"].chartarea = "ChartArea1"
    $chart1.Series["NonCompliant"].Legend = "Legend1"
    $chart1.Series["NonCompliant"].color = "#E74C3C"
    $chart1.Series["NonCompliant"].IsValueShownAsLabel = $true
    $datasource | ForEach-Object { $chart1.Series["NonCompliant"].Points.addxy( $_.Month , $_.NonCompliant) }


    # data series
    [void]$chart1.Series.Add("Unknown")
    $chart1.Series["Unknown"].ChartType = "StackedColumn100"
    $chart1.Series["Unknown"].IsVisibleInLegend = $true
    $chart1.Series["Unknown"].BorderWidth = 10
    $chart1.Series["Unknown"].chartarea = "ChartArea1"
    $chart1.Series["Unknown"].Legend = "Legend1"
    $chart1.Series["Unknown"].color = "#CACFD2"
    $chart1.Series["Unknown"].IsValueShownAsLabel = $true
    $datasource | ForEach-Object { $chart1.Series["Unknown"].Points.addxy( $_.Month , $_.Unknown) }
 
    # save chart
    $chart1.SaveImage("$PSScriptRoot\PatchCompliance_Stacked100.png", "png")
}


# This function is used to send the email containing the report and attachments. 
# The reason this one is used instead of Send-MailMessage is that .NET allows images to be embedded in the email using the cid tag

Function SendMail {

    $mailbody = $args[0]
    $mailattachments = $args[1]

    $SmtpClient = New-Object System.Net.Mail.SmtpClient($smtpServer)
    $MailMessage = New-object System.Net.Mail.MailMessage($mailinfo_from, $mailinfo_to, $Subject, $mailbody)
    $MailMessage.IsBodyHtml = $true


    $trimlenght = $PSScriptRoot.Length + 1 #have to add 1 because of the extra / in the path

    Foreach ($attachment in $mailattachments) {

        $attachmentID = $attachment.SubString($trimlenght) #This is to use only the actualy filename as ID
        $obj_attachment = New-Object System.Net.Mail.Attachment($attachment)
        $obj_attachment.ContentID = $attachmentID
        $MailMessage.Attachments.Add($obj_attachment)

    }

    $SmtpClient.Send($MailMessage)

}

$Attachments = Add-Attachment "$PSScriptRoot\$logo"


#Get all Computers within the specified SearchBase excluding Servers
$Computers = Get-ADComputer -SearchBase $ADSearchBase -filter { (OperatingSystem -notlike "*Server*") } -properties Name, DistinguishedName, OperatingSystem, OperatingSystemServicePack, passwordLastSet, LastLogonDate, Description

AddTo-Table $tbl_ADSummary "ADHeading" "Number of computers in $OUName" "ADData" $Computers.Count

If ($printToConsole) {
    Write-Host "Overview:"
    Write-Host "--------------------"
    Write-Host "Total number of computers: "$Computers.Count
}



#Count number of unique OS
If ($runOSCount -or $runEverything) {
    #Change variable to UComputers and work with this
    $UComputers = $Computers | select-object Name, OperatingSystem, OperatingSystemServicePack, passwordLastSet, LastLogonDate, DistinguishedName, CountryOUADvariable

    $UniqueOS = $UComputers | Select-Object OperatingSystem | Sort OperatingSystem -Descending | Get-Unique -Asstring

    #Count number of unique OS
    $NumberofUniqueOS = 0
    foreach ($uos in $UniqueOS) { $NumberofUniqueOS++ }

    #AddTo-Table $tbl_ADSummary "ADHeading" "Number of different Operating Systems" "ADData" $NumberofUniqueOS


    #Meassure/Count how many computer object of each OS type

    foreach ($os in $UComputers | Select-Object OperatingSystem | Sort OperatingSystem -Descending | Get-Unique -Asstring) {
        $count = ($UComputers | where { $_.OperatingSystem.ToString() -eq $os.OperatingSystem } | measure).Count
        AddTo-Table $tbl_ostable "OSName" ($os.OperatingSystem.ToString()) "OSCount" $count
        $count = 0
    }
    #CreateOSChart $tbl_ostable
    CreateChart $tbl_ostable "Operating Systems" "Pie" "OSName" "OSCount" "$PSScriptRoot\OS_Pie.png" "400" "200" $true

    $Attachments = Add-Attachment "$PSScriptRoot\OS_Pie.png"

}

CD $SiteCode':'


#Finding Computers that are within and outside the treshold of when Computer password was last set
If ($runComputerPasswordCount -or $runEverything) {

    $ComputerStaleDate = (Get-Date).AddDays(-$ComputerPasswordAgeDays)

    $PWWithin = $Computers | Where { $_.passwordLastSet -ge $ComputerStaleDate }
    $PWOutside = $Computers | Where { $_.passwordLastSet -le $ComputerStaleDate }

    AddTo-Table $tbl_piechart "Category" "Within Treshold" "Amount" $PWWithin.Count
    AddTo-Table $tbl_piechart "Category" "Outside Treshhold" "Amount" $PWOutside.Count

    If ($printToConsole) {
        Write-Host "Computer Password:"
        Write-Host "--------------------"
        Write-Host "Within Treshold: "$PWWithin.Count
        Write-Host "Outside Treshhold: "$PWOutside.Count
    }

    #CreateComputerPWChart $tbl_piechart

    CreateChart $tbl_piechart "Computer Password" "Pie" "Category" "Amount" "$PSScriptRoot\ComputerPW_Pie.png" "400" "200" $true

    $ComputersinSCCM = $PWOutside | Where-Object { $_ -ne $null } | Foreach-Object { Get-CMDevice -Name $_.Name }
    AddTo-Table $tbl_ADSummary "ADHeading" "In SCCM but havent changed PW within $ComputerPasswordAgeDays days" "ADData" $ComputersinSCCM.Count

    $Attachments = Add-Attachment "$PSScriptRoot\ComputerPW_Pie.png"
}

#Count number of computers that havent been rebooted within the number of days specified in $RebootTresholdDays
If ($runRebootCount -or $runEverything) {
    $OUCanonicalName = (Get-ADOrganizationalUnit -Filter * -Properties CanonicalName -SearchBase "$ADSearchBase" -SearchScope Base).CanonicalName
    
    $computersNotRebooted = (Get-WmiObject -namespace $SCCMNameSpace -computer $CMSiteServerFQDN -query "SELECT DISTINCT SMS_R_System.Name, SMS_G_System_OPERATING_SYSTEM.LastBootUpTime, SMS_G_System_OPERATING_SYSTEM.Caption
    FROM SMS_R_System INNER JOIN SMS_G_System_OPERATING_SYSTEM ON SMS_G_System_OPERATING_SYSTEM.ResourceID = SMS_R_System.ResourceId
    WHERE (DateDiff(day, SMS_G_System_OPERATING_SYSTEM.LastBootUpTime, GetDate()) >$RebootTresholdDays) AND SMS_R_System.SystemOUName = '$OUCanonicalName'").SMS_R_System | Select Name

    AddTo-Table $tbl_ADSummary "ADHeading" "Not rebooted within the last $RebootTresholdDays days" "ADData" $computersNotRebooted.Count

    If ($printToConsole) {
        Write-Host "Computer Reboot:"
        Write-Host "Computers not rebooted with $RebootTresholdDays : " $computersNotRebooted.Count

    }

}

#Get the last 3 SoftwareUpdate Groups
If ($runPatchCompScan -or $runEverything) {

    
    $SUGs = Get-CMSoftwareUpdateGroup | Sort-Object { $_.datecreated } -Descending | Select -First $patchNumberOfMonth

    If ($printToConsole) {
        Write-Host "Software Update Groups:"
        Write-Host "-----------------------"
    }

    Foreach ($sug in $SUGs) {

        $month = $sug.LocalizedDisplayName
        $compliant = [math]::Round((($sug.NumCompliant / ($sug.NumTotal)) * 100), 2)
        $noncompliant = [math]::Round((($sug.NumNonCompliant / ($sug.NumTotal)) * 100), 2)
        $unknown = [math]::Round((($sug.NumUnknown / $sug.NumTotal) * 100), 2)

        If ($printToConsole) {
            Write-Host "Month: $month"
            Write-Host "-Compliant: $Compliant"
            Write-Host "-NonCompliant: $noncompliant"
            Write-Host "-Unknown: $unknown"
        }
        

        $row = $tbl_Compliance.NewRow(); $row.Month = $month; $row.Compliant = $compliant; $row.NonCompliant = $noncompliant; $row.Unknown = $unknown
        $tbl_Compliance.Rows.Add($row)

    }


    CreateComplianceChart $tbl_Compliance

    $Attachments = Add-Attachment "$PSScriptRoot\PatchCompliance_Stacked100.png"


}

#Count number of Applications in SCCM
If ($runAppScan -or $runEverything) {
    $AllApps = Get-CMApplication
    $DeployedSuperseeded = $AllApps | Where-Object { <#($_.IsDeployed -eq $true) -and #>($_.IsSuperseded -eq $true) } 
    $DeployedExpired = $AllApps | Where-Object { <#($_.IsDeployed -eq $true) -and#> ($_.IsExpired -eq $true) }
    $DeployedWorking = $AllApps | Where-Object { ($_.IsDeployed -eq $true) -and ($_.IsExpired -eq $false) -and ($_.IsSuperseded -eq $false) }
    $NotDeployed = $AllApps | Where-Object { ($_.IsDeployed -eq $false) }

  
    #AddTo-Table $tbl_AppsSummary "AppsHeading" "Total number of Applications in SCCM" "AppsData" $AllApps.Count
    AddTo-Table $tbl_AppsSummary "AppsHeading" "Superseeded Applications" "AppsData" $DeployedSuperseeded.Count
    AddTo-Table $tbl_AppsSummary "AppsHeading" "Expired Applications" "AppsData" $DeployedExpired.Count
    AddTo-Table $tbl_AppsSummary "AppsHeading" "Active Applications" "AppsData" $DeployedWorking.Count
    AddTo-Table $tbl_AppsSummary "AppsHeading" "Applications without deployment(s)" "AppsData" $NotDeployed.Count
    
    CreateChart $tbl_AppsSummary "All Applications" "Pie" "AppsHeading" "AppsData" "$PSScriptRoot\Applications_Pie.png" "400" "200" $true

    $Attachments = Add-Attachment "$PSScriptRoot\Applications_Pie.png"

    If ($printToConsole) {
        Write-Host "Applications:"
        Write-Host "--------------------"
        Write-Host "Superseeded Applications: "$DeployedSuperseeded.Count
        Write-Host "Expired Applications: "$DeployedExpired.Count
        Write-Host "Active Applications: "$DeployedWorking.Count
        Write-Host "Applications without deployment(s): "$NotDeployed.Count
    }

}

#Count devices in SCCM
If ($runClientScan -or $runEverything) {

    $AllDevicesinSCCM = Get-CMDevice -CollectionName "$Collection"
    $AciveDevices = $AllDevicesinSCCM | Where-Object { ($_.ClientActiveStatus -eq 1) }
    $InaciveDevices = $AllDevicesinSCCM | Where-Object { ($_.ClientActiveStatus -eq 0) }
    $ObsoleteDevices = $AllDevicesinSCCM | Where-Object { ($_.IsObsolete -eq $true) }
    $NoSCCMClient = $AllDevicesinSCCM | Where-Object { ($_.IsClient -eq $false) }

    AddTo-Table $tbl_DeviceSummary "DeviceHeading" "Active Devices" "DeviceData" $AciveDevices.Count
    AddTo-Table $tbl_DeviceSummary "DeviceHeading" "Obsolete devices" "DeviceData" $ObsoleteDevices.Count
    AddTo-Table $tbl_DeviceSummary "DeviceHeading" "Inactive Devices" "DeviceData" $InaciveDevices.Count
    AddTo-Table $tbl_DeviceSummary "DeviceHeading" "Devices without Client" "DeviceData" $NoSCCMClient.Count

    CreateChart $tbl_DeviceSummary "Devices in $Collection" "Pie" "DeviceHeading" "DeviceData" "$PSScriptRoot\Devices_Pie.png" "400" "200" $true

    $Attachments = Add-Attachment "$PSScriptRoot\Devices_Pie.png"

    If ($printToConsole) {
        Write-Host "Devices:"
        Write-Host "--------------------"
        Write-Host "Active Devices: "$AciveDevices.Count
        Write-Host "Obsolete devices: "$ObsoleteDevices.Count
        Write-Host "Inactive Devices: "$InaciveDevices.Count
        Write-Host "Devices without Client: "$NoSCCMClient.Count
    }
}

CreateChart $tbl_ADSummary "Computer Info" "Bar" "ADHeading" "ADData" "$PSScriptRoot\ComputerInfo_Bar.png" "800" "300" $false

$Attachments = Add-Attachment "$PSScriptRoot\ComputerInfo_Bar.png"


#Convert All tables to HTML
#--------------------------------------------------------

#Convert the ostable to a HTML Fragment that can be inserted into the html document.
$osfragment = $tbl_ostable | Select @{Name = "Operating System"; Expression = { $_.OSName } }, @{Name = "Count"; Expression = { $_.OSCount } } | ConvertTo-Html -Fragment
$adfragment = $tbl_ADSummary | Select ADHeading, ADData | ConvertTo-Html -Fragment
$sccmfragment = $tbl_AppsSummary | Select AppsHeading, AppsData | ConvertTo-Html -Fragment

#Write results depending on parameter set
$footer = "Report ran from {3} initiated by {1}\{2} at {0}" -f (Get-Date), $env:USERDOMAIN, $env:USERNAME, $env:COMPUTERNAME


$fragments = @()

#Insert a graphic header
$fragments += "<Center><table>"
$fragments += "<tr><td><Img src='cid:$logo'></td></tr>"
$fragments += "<tr><td><H1>$Customername - Environment Summary</H1></td></tr>"
$fragments += "<tr><td ><h3>Computers in: $OUName</h3></td></tr>"

#Only show this section if any of the elements in it are choosen to be calculated
If ($runRebootCount -or $runComputerPasswordCount -or $runOSCount -or $runEverything) { 
    $fragments += "<tr><th><H2>Active Directory Summary</H2></th></tr>"

    If ($runRebootCount -or $runEverything) { 
        $fragments += "<tr><td><Img src='cid:ComputerInfo_Bar.png'></td></tr>"

    }

    If ($runComputerPasswordCount -or $runOSCount -or $runEverything) { 
        $fragments += "<tr><td>"
        If ($runOSCount -or $runEverything) {
            $fragments += "<Img src='cid:OS_Pie.png'>"
        }
        If ($runComputerPasswordCount -or $runEverything) {           
            $fragments += "<Img src='cid:ComputerPW_Pie.png'>"
        }
        
        $fragments += "</td></tr>"
    }

}
#Only show the SCCM Summary in the report if either runAppScan or runClientScan is enabled.
If ($runAppScan -or $runClientScan -or $runEverything) {
    $fragments += "<tr><th><H2>SCCM Summary</H2></th></tr>"

   
    $fragments += "<tr><td>"
    If ($runAppScan -or $runEverything) {
        $fragments += "<Img src='cid:Applications_Pie.png'>"
    }
    If ($runClientScan -or $runEverything) {           
        $fragments += "<Img src='cid:Devices_Pie.png'>"
    }
        
    $fragments += "</td></tr>"
  
}

#Only show the Patch Compliance Overview if runPatchCompScan is enabled
If ($runPatchCompScan -or $runEverything) {
    $fragments += "<tr><th><H2>Patch Compliance Overview</H2></th></tr>"
    $fragments += "<tr><td><Img src='cid:PatchCompliance_Stacked100.png'></td></tr>"
}

$fragments += "<tr><td><p>$footer</p></td></tr>"
$fragments += "</table></Center>"


CD c:\


#Write to Files
$DeployedSuperseeded | Select @{Name = "Superseeded Applications"; Expression = { $_.LocalizedDisplayName } } | Out-File $File_ExpSuperseededApps
$DeployedExpired | Select @{Name = "Expired Applications"; Expression = { $_.LocalizedDisplayName } } | Out-File $File_ExpSuperseededApps -Append
$NotDeployed | Select @{Name = "Applications without Deployments"; Expression = { $_.LocalizedDisplayName } } | Out-File $File_ExpSuperseededApps -Append

$computersNotRebooted | Out-File $File_CompNotBooted

#Adding files to attachment array if they have been created.

If ($runRebootCount) {
    $Attachments = Add-Attachment $File_CompNotBooted
}

If ($runAppScan) {
    $Attachments = Add-Attachment $File_ExpSuperseededApps
}

$body = ConvertTo-Html -Head $head -Title $ReportTitle -Body $fragments

#For the html report the cid is removed so the image src path is correct
$report = $fragments -replace "cid:", ""

ConvertTo-Html -Head $head -Title $ReportTitle -Body $report | Out-File $OutFile

$ArrayAttachment = $Attachments.Split(",")

If ($sendMail) {
    sendmail $body $ArrayAttachment
}
Else {
    Write-Host "Send Mail is disabled so no mail were sent."
}

Script to Report on Recent Deployments

This Script can be used to automate reporting on application deployment. With the current settings it gathers deployment summary for the deployments that was created within the last 14 days and sends an email to the defined recipient(s).

All you have to do is to change the variables under #Mail and #Variables to match your domain settings and verifiy the import-module path.

After that you can set up a scheduled task that runs this script with the frequency that you want.

#Mail
$smtpServer = "smtprelay.domain.com"
$mailinfo_from = "server@domain.com"
$mailinfo_to = "initials@domain.com", "initials2@domain.com"
$mailinfo_cc = ""
$subject = "Deployment Summary Report"
$logoURL = "<Img src='https://www.url.com/picture.gif' style='float:left' width='99' height='75' hspace=10>"

#Variables
$ComputerName="sccmserver.domain.com"
$SiteCode = "SiteCode"
$ReportTitle="System Report"
$period = 14
$OutFile = "$PSScriptRoot\DeploymentReport.html"

Import-Module '\\$ComputerName\d$\Program Files\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1'

$head = @"
<style>
TABLE {border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;margin-left:50px}
TH {border-width: 1px;padding: 3px;border-style: solid;border-color: black;background-color: #8c0000;}
TD {border-width: 1px;padding: 3px;border-style: solid;border-color: black;}
.odd  { background-color:#ffffff; }
.even { background-color:#dddddd; }
</style>
<Title>$ReportTitle</Title>
"@

#Computer system
$cs = Get-WmiObject -Class Win32_Computersystem -ComputerName $computername
$csdata = $cs | Select Status,Manufacturer,Model,SystemType,Number*

#Deployment Summary
$timeSpan = (Get-Date).Adddays(-$period)
$GetDeployments = Get-WmiObject -Namespace root\sms\site_$SiteCode -Class SMS_DeploymentSummary -ComputerName $ComputerName | Where-Object {($_.FeatureType -eq "1") -AND ($_.CollectionName -like "*-install (device)") -AND ($_.CollectionName -like "*-install (device)") -AND ($_.ConvertToDateTime($_.CreationTime) -gt $timeSpan)}
$deploymentSummary = $GetDeployments | Select-Object CollectionName,@{Label="CreationTime";Expression={$_.ConvertToDateTime($_.CreationTime)}},@{Label="Deployment";Expression={$_.SoftwareName}},NumberSuccess,NumberInProgress,NumberOther,NumberErrors,FeatureType
cd dd1:
$deploymentSummary=$deploymentSummary | Where-Object {(Get-CMApplication -Name $_.Deployment).IsSuperseded -eq $false}

#Write results depending on parameter set
$footer="Report ran from {3} initiated by {1}\{2}" -f (Get-Date),$env:USERDOMAIN,$env:USERNAME,$env:COMPUTERNAME

#Prepare HTML code
$fragments = @()

#Insert a graphic header
$fragments += "$logoURL<br><br>"
$fragments += $deploymentsummary | ConvertTo-HTML -Fragment -PreContent "<H2>Deployment Summary last $period days</H2>"

Write $fragments | clip
cd c:\
ConvertTo-Html -Head $head -Title $ReportTitle -PreContent ($fragments | out-String) -PostContent "<br><br><font color='red'>Superseeded Deployments are not displayed even tho they have been created within the last $period days</font><br><br><I>$footer</I>" | Out-File $OutFile

$Body = Get-Content $OutFile

#If there are no deployments created within the treshhold dont send a mail
If ($deploymentSummary.Count -notlike 0){
    Send-MailMessage -SmtpServer $smtpServer -To $mailinfo_to -From $mailinfo_from -Subject $subject -Body "$Body" -BodyAsHtml -Attachments $OutFile
}
Get-Item $OutFile | Remove-Item -Force

Script to Report on Software Update Group compliance

This script will collect the status of the last created Software Update Group and send it as a mail to the specified recipient(s).

All you have to do is to change the variables under #Mail and #Variables to match your domain settings.

After that you can set up a scheduled task that runs this script with the frequency that you want.

Import-Module '\\sccmserver\d$\Program Files\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1'

#Mail
$smtpServer = "smtprelay.customer.com"
$mailinfo_from = "server1@customer.com"
$mailinfo_to = "initials@company.com"
$mailinfo_cc = ""
$subject = "Windows 10 Patch Compliance - "

#Variables
$tableheading = "Windows 10 Patch Compliance - "
$logoURL = "<Img src='https://www.url.com/picture.gif' style='float:left' width='99' height='75' hspace=10>"
[string]$SavePath = "c:\temp\Compliance Reports"
$year = (Get-Date).year
$month = Get-Date -Format MM
$SiteCode = "SiteCode"


$ReportTitle = "System Report"
[string]$reportVersion = "1.0"


$head = @"
<style>
TABLE {border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;margin-left:50px}
TH {border-width: 1px;padding: 3px;border-style: solid;border-color: black;background-color: #8c0000;}
TD {border-width: 1px;padding: 3px;border-style: solid;border-color: black;}
.odd  { background-color:#ffffff; }
.even { background-color:#dddddd; }
</style>
<Title>$ReportTitle</Title>
"@


#Access the SCCM Site
Set-Location $SiteCode:

#Finding the latest created Software Update Group

$SUG = Get-CMSoftwareUpdateGroup | Sort-Object { $_.datecreated } -Descending | Select -First 1

$updategroupname = $SUG.LocalizedDisplayName

#Getting all updates on that Software update Group

$Updates = (Get-CMSoftwareUpdate -UpdateGroupName $updategroupname) | Select-Object Vendor, ArticleID, BulletinID, @{E = { $_.LocalizedDisplayName }; L = "Titel" }, @{E = { $_.NumPresent }; L = "Installed" }, @{E = { $_.NumMissing }; L = "Required" }, @{E = { $_.NumNotApplicable }; L = "Not Required" } , @{E = { $_.NumUnknown }; L = "Unknown" }, @{E = { $_.NumTotal }; L = "Total" }, @{E = { [math]::Round((($_.NumPresent / ($_.NumPresent + $_.NumMissing)) * 100), 2) }; L = "% Compliant" }, @{E = { [math]::Round((($_.NumMissing / ($_.NumPresent + $_.NumMissing)) * 100), 2) }; L = "% Not Compliant" }, @{E = { [math]::Round((($_.NumUnknown / $_.NumTotal) * 100), 2) }; L = "% Unknown" }

#Write results depending on parameter set
$footer = "Report ran from {3} initiated by {1}\{2}" -f (Get-Date), $env:USERDOMAIN, $env:USERNAME, $env:COMPUTERNAME

#Prepare HTML code
$fragments = @()

#Insert a graphic header
$fragments += "$logoURL<br><br>"

$fragments += $Updates | ConvertTo-HTML -Fragment -PreContent "<H2>$tableheading $updategroupname</H2>"


write $fragments | clip
$outFile = ConvertTo-Html -Head $head -Title $ReportTitle -PreContent ($fragments | out-String) -PostContent "<br><I>$footer</I>"


#Checks savepath and export file.
IF ((test-path $SavePath) -eq $False) { md $SavePath }
$ExportFile = "$SavePath\Win 10 Compliance-$updategroupname.csv"
IF (Test-Path $ExportFile) { Remove-Item $ExportFile }


$Updates | export-csv $ExportFile -Append -Force -NoTypeInformation

$body = "" + $OutFile

Send-MailMessage -SmtpServer $smtpServer -From $mailinfo_from -To $mailinfo_to -Subject "$subject $updategroupname" -Body $body -BodyAsHtml -Attachments $ExportFile

Time sync during OS Deployment

After Microsoft Surface arrived in our production environment we have been challenged by time sync issues during OS deployment. The Surface devices were so much off in date and time that they were rejected by the management point resulting in a failed Task Sequence.

Instead of powering up the Surface and adjusting the time manually i wanted to automate this during deployment. I found out that Niall Brady have made a timesync script that can sync the time on the client with a server using NET TIME.

Adding this script to the prestart command on the Boot image will then sync the time with the server specified prior to running the task sequence and voila – problem solved.

' (c) niall@windows-noob.com 2014/7/24
' This script allows you to sync the time in WinPE with a local server using a prestart command
' Fill in the user/password/domain/server details below
' For more guides see http://www.windows-noob.com/forums/index.php?/topic/4045-system-center-2012-configuration-manager-guides/ 
' for troubleshooting, review the log files created
' net use (to see all existing connections)
' net use * /del /yes (to delete all existing connections) 

Option Explicit
DIM ComSpec, WshShell, strUser, strPassword, strDomain, strServer, strCommand, LogFileName, logfile, objFSO, objFile, outFile

Set WshShell = CREATEOBJECT("WScript.Shell")
strUser = "SCCM_DJ"
strPassword = "DJpassw0rd"
strDomain = "domain.com"
strServer = "server"

PrepLogFile
Logtext "Starting logging process."
LogText "sleeping for 15 seconds.."
WScript.Sleep 5000
LogText "Using NET USE to Connect to " & strServer & " as a Domain user.."
strCommand = ComSpec & ("cmd /c net use \\") & strServer & ("\ipc$") & (" ") & strPassword & (" ") & ("/user:") & strDomain & ("\") & strUser & (" ") & (">") & (" ") & ("x:\WinPE_net_use.log")

		If WshShell.Run (strCommand, 0, True) = 0 then
			LogText "...NET USE connected ok, continuing."
			'On Error GoTo 0
		else
			LogText "...NET USE had an error."
			ExitScript -1
		end if

LogText "Using the NET TIME command to sync with the Server time.."
strCommand = ("cmd /c net time \\") & strServer & (" /SET /Y") & (">") & (" ") & ("x:\WinPE_net_time.log")
WshShell.Run strCommand

'wait for 5 seconds so the new time is registered before talking to the mp
LogText "Waiting for 5 seconds so the new time is registered before talking to the mp.."

LogText "all done, exiting.." 

' =====================================================
' PrepLogFile Subroutine
' =====================================================

Sub PrepLogFile
	
	Dim objFSO

	Set wShShell = WScript.CreateObject("WScript.Shell")
	LogFileName = "X:\WinPE_TimeSync.log"
	'LogFileName = "c:\tmp\WinPE_TimeSync.log"

	'On Error Resume Next
	Err.Clear

	Set objFSO = CreateObject("Scripting.FileSystemObject")
	
	If Err.number <> 0 Then
		MsgBox("****   ERROR (" & Err.Number & ") Could not create Logfile - exiting script")
		ExitScript 0
	Else
		If objFSO.FileExists(LogFileName) Then
			objFSO.DeleteFile(LogFileName) 
		End If
		Err.Clear
		Set logfile = objFSO.CreateTextFile(LogFileName)
		If Err.number <> 0 Then
			MsgBox "ERROR (" & Err.Number & ") Could not create logfile (File) - exiting script"
			ExitScript 0
		End If
	End If
	
	Err.Clear
	
'On Error GoTo 0
	
	logfile.writeline "##############################################"
	logfile.writeline "    windows-noob.com WinPE Time sync Script   "
	logfile.writeline "##############################################"
End Sub

' =====================================================
' LogText Subroutine
' =====================================================

Sub LogText (TextToLog)
	logfile.writeline "" & Now() & " " & TextToLog
End Sub

' =====================================================
' Exit function
' =====================================================

Function ExitScript(iStatus)
	if iStatus <> 0 then
		set WshShell = WScript.CreateObject("WScript.Shell")
		ComSpec = WshShell.ExpandEnvironmentStrings("%COMSPEC%")
		WshShell.Run "cmtrace.exe " & LogFileName , 1, False
	End if

	LogText "All done, exiting successfully"
	wscript.quit(iStatus)
End Function