Category Archives: ConfigMgr

Note to self: WMI does not adhere to RPC-standards

Every single project…. every single firewall guy, and every single requirement list that I had to dig into…

Windows Management Traffic leverages the same ports as RPC traffic (TCP 135 for initial connection, and after that a random port within a defined port-range), however it does not adhere to the RPC specification and will therefore not be correctly identified by any firewall (yes, any firewall) as RPC traffic. Most firewalls tries to dynamically identify the specific port for the session within the dynamic range, however this requires that lots of things are RPC and not MSRPC.

Cisco wrote it pretty clearly;

As Microsoft switched from using pure RPC to use DCOM (ORPC) calls, those non-epm calls will be used more and more. Windows RPC/DCOM services use the RPC Endpoint Mapper to accept initial communications on port 135 and then dynamically transition to ports for the service.

Just open all the high-ports.

Checkpoint statement

Cisco statement

Troubleshoot:

Testing RPC ports with PowerShell (and yes, it’s as much fun as it sounds)

Wireshark-article if you ever need to troubleshoot

 

Software Metering–Checks and balances

Software Metering is a very interesting concept, however after been hit with quite a few issues of ensuring that we can depend on the reliable information within Configuration Manager – here comes an appendix to a great blog-post series provided by Microsoft

Software Metering Deep Dive and Automation Part 1: Use It Or Lose It – The Basics

Software Metering Deep Dive and Automation Part 2: Use It Or Lose It – The Collections

What is the issue?

For many reasons a few perfect storms essentially gave clients that from a far looked OK, but we could not consider their metering data accurate. Either it was blank (not the same as NULL), or it wasn’t good enough for only specific periods which caused them to inaccurately fall in-scope of not using a software at all. All of the reasons are of course part of the ongoing operations to ensure client health. However, users still had software removed and felt the frustration.

Checks and balances

We felt very confident in removing software, deploying accurately to devices which hadn’t use the software – apart from users being impacted by general issues of reporting metering data.

A check is to verify that there is metering data for the period that we want to ensure is software is used within. The following benefits are identified;

  • New devices wouldn’t be targeted until they have metering for the entire period
  • Devices which have black-out periods and fail to report any metering data for an extended period of time are excluded
  • Devices which are plain broken are excluded

Collections

Structure is to create a collection for each time period (month based) that you want to check. The specific example will use the following sample scenario;

  • A device will have the software removed if not used for 3 months (90 days)
  • We will verify that the device have metering data for period 0-30 days, 30-60 days and 60-90 days
  • The most efficient way to run the collection update is actually to have three separate collections
  • Each collection can be limited to the previous one
  • The collection with the deployment targeted can be limited to the final collection

Collection queries;

Metering check day 0 – 30

select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client
from SMS_R_System  INNER JOIN SMS_MonthlyUsageSummary on SMS_R_SYSTEM.ResourceID = SMS_MonthlyUsageSummary.ResourceID
WHERE (DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) < 30
and DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) > 0)

Metering check day 30 – 60

select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client
from SMS_R_System  INNER JOIN SMS_MonthlyUsageSummary on SMS_R_SYSTEM.ResourceID = SMS_MonthlyUsageSummary.ResourceID
WHERE (DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) < 60
and DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) > 30)

Metering check day 60 – 90

select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client
from SMS_R_System  INNER JOIN SMS_MonthlyUsageSummary on SMS_R_SYSTEM.ResourceID = SMS_MonthlyUsageSummary.ResourceID
WHERE (DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) < 90
and DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) > 60)

AppsNotify 3.0

As a follow-up to a previous post – here comes a revised version of AppsNotify.

Changes

  • Possible to exclude applications that notify users
    Create a registry value under HKLM\Software\AppsNotify\AppsNotifyExclusion that matches the application that needs to be excluded
  • Detect if the C: – drive is low on disk and exit before attempt to write anything to disk
  • Notifications now include name of applications that are new, up to 5 new applications
  • Detect if the computer is idle and do not check AppCatalog and do not notify user
    -Avoids hammering of the website and notifications going nowhere
  • Log-file is generated in users temp folder: \appsnotify app.log

Code from elsewhere

Idle-time from StackOverflow
Most functions (not Logs – as previously stated in old blog-post) are from PowerShell Studio
Or written by me. See previous post.

Parameter

Pass on the parameter appcatalog with the url for Application Catalog – sample;

-appcatalog http://appcatalog.yourcompany.com:81/cmapplicationCatalog

Run it

Normally I wrap this within an executable that file (can run with Powershell 2/3 and should run as logged on user). How often? Well – sample code from an exported Task Scheduler

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="<a href="http://schemas.microsoft.com/windows/2004/02/mit/task&quot;">http://schemas.microsoft.com/windows/2004/02/mit/task"</a>>
<RegistrationInfo>
<Author></Author>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Repetition>
<Interval>PT10M</Interval>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
<StartBoundary>1899-12-30T06:04:14</StartBoundary>
<Enabled>true</Enabled>
<Delay>PT15M</Delay>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<GroupId>S-1-5-32-545</GroupId>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>true</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT4H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>"C:\Program Files (x86)\Common Files\AppsNotify\AppsNotify.exe"</Command>
<Arguments>-appcatalog http://website:8080/cmapplicationCatalog</Arguments>
</Exec>
</Actions>
</Task>

Code

Or download it

#========================================================================
# Created on:    2017-08-16
# Created by:    Nicke Källén
# Organization:
# Filename:        AppsNotify 3.0.pff
#========================================================================
Add-Type @'
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace PInvoke.Win32 {

public static class UserInput {

[DllImport("user32.dll", SetLastError=false)]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);

[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO {
public uint cbSize;
public int dwTime;
}

public static DateTime LastInput {
get {
DateTime bootTime = DateTime.UtcNow.AddMilliseconds(-Environment.TickCount);
DateTime lastInput = bootTime.AddMilliseconds(LastInputTicks);
return lastInput;
}
}

public static TimeSpan IdleTime {
get {
return DateTime.UtcNow.Subtract(LastInput);
}
}

public static int LastInputTicks {
get {
LASTINPUTINFO lii = new LASTINPUTINFO();
lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
GetLastInputInfo(ref lii);
return lii.dwTime;
}
}
}
}
'@

$AppNotify.FormBorderStyle = 'FixedToolWindow'
Function Log-Start{
<#
.SYNOPSIS
Creates log file

.DESCRIPTION
Creates log file with path and name that is passed. Checks if log file exists, and if it does deletes it and creates a new one.
Once created, writes initial logging data

.PARAMETER LogPath
Mandatory. Path of where log is to be created. Example: C:\Windows\Temp

.PARAMETER LogName
Mandatory. Name of log file to be created. Example: Test_Script.log

.PARAMETER ScriptVersion
Mandatory. Version of the running script which will be written in the log. Example: 1.5

.INPUTS
Parameters above

.OUTPUTS
Log file created

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support

.EXAMPLE
Log-Start -LogPath "C:\Windows\Temp" -LogName "Test_Script.log" -ScriptVersion "1.5"
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LogName, [Parameter(Mandatory=$true)][string]$ScriptVersion)

Process{
$sFullPath = $LogPath + "\" + $LogName

#Check if file exists and delete if it does
If((Test-Path -Path $sFullPath)){
Remove-Item -Path $sFullPath -Force
}

#Create file and start logging
New-Item -Path $LogPath -Name $LogName –ItemType File

Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value "Started processing at [$([DateTime]::Now)]."
Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value ""
Add-Content -Path $sFullPath -Value "Running script version [$ScriptVersion]."
Add-Content -Path $sFullPath -Value ""
Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value ""

#Write to screen for debug mode
Write-Debug "***************************************************************************************************"
Write-Debug "Started processing at [$([DateTime]::Now)]."
Write-Debug "***************************************************************************************************"
Write-Debug ""
Write-Debug "Running script version [$ScriptVersion]."
Write-Debug ""
Write-Debug "***************************************************************************************************"
Write-Debug ""

}
}

Function Log-Write{
<#
.SYNOPSIS
Writes to a log file

.DESCRIPTION
Appends a new line to the end of the specified log file

.PARAMETER LogPath
Mandatory. Full path of the log file you want to write to. Example: C:\Windows\Temp\Test_Script.log

.PARAMETER LineValue
Mandatory. The string that you want to write to the log

.INPUTS
Parameters above

.OUTPUTS
None

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support

.EXAMPLE
Log-Write -LogPath "C:\Windows\Temp\Test_Script.log" -LineValue "This is a new line which I am appending to the end of the log file."
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LineValue)

Process{
Add-Content -Path $LogPath -Value $LineValue

#Write to screen for debug mode
Write-Debug $LineValue
}
}

Function Log-Error{
<#
.SYNOPSIS
Writes an error to a log file

.DESCRIPTION
Writes the passed error to a new line at the end of the specified log file

.PARAMETER LogPath
Mandatory. Full path of the log file you want to write to. Example: C:\Windows\Temp\Test_Script.log

.PARAMETER ErrorDesc
Mandatory. The description of the error you want to pass (use $_.Exception)

.PARAMETER ExitGracefully
Mandatory. Boolean. If set to True, runs Log-Finish and then exits script

.INPUTS
Parameters above

.OUTPUTS
None

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support. Added -ExitGracefully parameter functionality

.EXAMPLE
Log-Error -LogPath "C:\Windows\Temp\Test_Script.log" -ErrorDesc $_.Exception -ExitGracefully $True
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$ErrorDesc, [Parameter(Mandatory=$true)][boolean]$ExitGracefully)

Process{
Add-Content -Path $LogPath -Value "Error: An error has occurred [$ErrorDesc]."

#Write to screen for debug mode
Write-Debug "Error: An error has occurred [$ErrorDesc]."

#If $ExitGracefully = True then run Log-Finish and exit script
If ($ExitGracefully -eq $True){
Log-Finish -LogPath $LogPath
Break
}
}
}

Function Log-Finish{
<#
.SYNOPSIS
Write closing logging data & exit

.DESCRIPTION
Writes finishing logging data to specified log and then exits the calling script

.PARAMETER LogPath
Mandatory. Full path of the log file you want to write finishing data to. Example: C:\Windows\Temp\Test_Script.log

.PARAMETER NoExit
Optional. If this is set to True, then the function will not exit the calling script, so that further execution can occur

.INPUTS
Parameters above

.OUTPUTS
None

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support

Version:        1.2
Author:         Luca Sturlese
Creation Date:  01/08/12
Purpose/Change: Added option to not exit calling script if required (via optional parameter)

.EXAMPLE
Log-Finish -LogPath "C:\Windows\Temp\Test_Script.log"

.EXAMPLE
Log-Finish -LogPath "C:\Windows\Temp\Test_Script.log" -NoExit $True
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$false)][string]$NoExit)

Process{
Add-Content -Path $LogPath -Value ""
Add-Content -Path $LogPath -Value "***************************************************************************************************"
Add-Content -Path $LogPath -Value "Finished processing at [$([DateTime]::Now)]."
Add-Content -Path $LogPath -Value "***************************************************************************************************"

#Write to screen for debug mode
Write-Debug ""
Write-Debug "***************************************************************************************************"
Write-Debug "Finished processing at [$([DateTime]::Now)]."
Write-Debug "***************************************************************************************************"

#Exit calling script if NoExit has not been specified or is set to False
If(!($NoExit) -or ($NoExit -eq $False)){
Exit
}

}
}
function Get-ScriptDirectory
{
if($hostinvocation -ne $null)
{
Split-Path $hostinvocation.MyCommand.path
}
else
{
Split-Path $script:MyInvocation.MyCommand.Path
}
}

function Parse-Commandline
{
<#
.SYNOPSIS
Parses the Commandline of a package executable

.DESCRIPTION
Parses the Commandline of a package executable

.PARAMETER  Commandline
The Commandline of the package executable

.EXAMPLE
$arguments = Parse-Commandline -Commandline $Commandline

.INPUTS
System.String

.OUTPUTS
System.Collections.Specialized.StringCollection
#>

[OutputType([System.Collections.Specialized.StringCollection])]
Param([string]$CommandLine)

$Arguments = New-Object System.Collections.Specialized.StringCollection

if($CommandLine)
{
#Find First Quote
$index = $CommandLine.IndexOf('"')

while ( $index -ne -1)
{#Continue as along as we find a quote
#Find Closing Quote
$closeIndex = $CommandLine.IndexOf('"',$index + 1)
if($closeIndex -eq -1)
{
break #Can’t find a match
}
$value = $CommandLine.Substring($index + 1,$closeIndex – ($index + 1))
[void]$Arguments.Add($value)
$index = $closeIndex

#Find First Quote
$index = $CommandLine.IndexOf('"',$index + 1)
}
}
return $Arguments
}

function Convert-CommandLineToDictionary
{
<#
.SYNOPSIS
Parses and converts the commandline of a packaged executable into a Dictionary

.DESCRIPTION
Parses and converts the commandline of a packaged executable into a Dictionary

.PARAMETER  Dictionary
The Dictionary to load the value pairs into.

.PARAMETER  CommandLine
The commandline of the package executable

.PARAMETER  ParamIndicator
The character used to indicate what is a parameter.

.EXAMPLE
$Dictionary = New-Object System.Collections.Specialized.StringDictionary
Convert-CommandLineToDictionary -Dictionary $Dictionary -CommandLine $Commandline  -ParamIndicator '-'
#>
Param(    [ValidateNotNull()]
[System.Collections.Specialized.StringDictionary]$Dictionary,
[string]$CommandLine,
[char] $ParamIndicator)

$Params = Parse-Commandline $CommandLine

for($index = 0; $index -lt $Params.Count; $index++)
{
[string]$param = $Params[$index]
#Clear the values
$key = ""
$value = ""

if($param.StartsWith($ParamIndicator))
{
#Remove the indicator
$key = $param.Remove(0,1)
if($index  + 1 -lt $Params.Count)
{
#Check if the next Argument is a parameter
[string]$param = $Params[$index + 1]
if($param.StartsWith($ParamIndicator) -ne $true )
{
#If it isn’t a parameter then set it as the value
$value = $param
$index++
}
}
$Dictionary[$key] = $value
}#else skip
}
}
function Get-ExclusionList
{
$REGExclusion = "HKLM:\Software\AppsNotify\AppsNotifyExclusion"
$excllist = @()
Log-Write -LogPath $logfilePath -LineValue "Start exclusion list creation"
If ($(Test-Path $REGExclusion))
{

##Discovery
$regKey = $REGExclusion

$p = Get-ItemProperty $REGExclusion

$p.PSObject.Properties |  foreach  { if (("PSPath","PSParentPath","PSChildName","PSDrive","PSProvider") -notcontains $_.name) { $excllist += $($_.name) } }

}
Log-Write -LogPath $logfilePath -LineValue $($excllist)
return $excllist
}

function Validate-IsURL
{
<#
.SYNOPSIS
Validates if input is an URL

.DESCRIPTION
Validates if input is an URL

.PARAMETER  Url
A string containing an URL address

.INPUTS
System.String

.OUTPUTS
System.Boolean
#>
[OutputType([Boolean])]
param ([string]$Url)

if($Url -eq $null)
{
return $false
}

return $Url -match "^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&amp;%\$#_]*)?$"
}

function Get-CMUserApps {
[CmdletBinding()]
param
(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='URL for Application Catalogue')]
$url,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Path to logfile')]
$logfile,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Temp-file')]
$temp
)
Begin {
log-write -LogPath $logfile -LineValue "Create web service proxy"
$catalogurl = $url;
Log-Write -LogPath $logfile -LineValue "Connecting to $catalogurl"
try {
$url = $catalogurl+"/ApplicationViewService.asmx?WSDL";
$service = New-WebServiceProxy $url -UseDefaultCredential;

}
catch {
Log-Error -LogPath $logfile -ErrorDesc "AppCatalog no response" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $true
break
}
}
Process {


$total = 0;
try {
Log-Write -LogPath $logfile -LineValue "Gathering applications"
$service.GetApplications("Name",$null,"Name","",100,0,$true,"PackageProgramName",$false,$null,[ref]$total) | select ApplicationId,Name | Export-Clixml $temp
return $true
}

catch {
Log-Error -LogPath $logfile -ErrorDesc $error[0] -ExitGraceFully $false
return $false
}
Remove-Variable -Name url
Remove-Variable -Name total
$service.dispose()

}

}

function Compare-CMUserApps {
[CmdletBinding()]
param
(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Permanent-file')]
$file,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Temp-file')]
$temp,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Path to logfile')]
$logfile
)
Process {
Log-Write -LogPath $logfile -LineValue "Comparing applications lists"
If (-Not (Test-Path $file)) {
Log-Write -LogPath $logfile -LineValue "No previous version of apps list"
try {
Rename-Item $temp "$prefix apps.xml"
}
catch {
Remove-Item $temp
Log-Error -LogPath $logfile -ErrorDesc "Unable to create initial list" -ExitGracefully $false
}

}
Else {

Log-Write -LogPath $logfile -LineValue "Starting check......"
# $diffs = (Compare-Object -ReferenceObject $(Get-Content $file) -DifferenceObject $(Get-Content $temp)) | Where {$_.SideIndicator -eq '<='}
# $diffsserver = (Compare-Object -ReferenceObject $(Get-Content $file) -DifferenceObject $(Get-Content $temp)) | Where {$_.SideIndicator -eq '=>'}

If ((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) -eq $null) {
Log-Write -LogPath $logfile -LineValue "No new applications"
Log-Write -LogPath $logfile -LineValue "Removing temporary file"

try {
Remove-Item $temp
}
catch {

Log-Error -LogPath $logfile -ErrorDesc "Unable to remove temp list" -ExitGracefully $false
}

}
Elseif (((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) | Where {$_.SideIndicator -eq '<='}) -ne $null -and ((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) | Where {$_.SideIndicator -eq '=>'}) -eq $null ) {
Log-Write -LogPath $logfile -LineValue "Less applications received"
try {
Log-Write -LogPath $logfile -LineValue "Remove permanent list"
Remove-Item $file
}
catch {
Remove-Item $temp
Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
}

try {
Log-Write -LogPath $logfile -LineValue "Rename temporary list"
Rename-Item $temp "$prefix apps.xml"
}
catch {
Log-Error -LogPath $logfile -ErrorDesc "Unable to switch temp-list to permanent" -ExitGracefully $false
}

}

Else {
Log-Write -LogPath $logfile -LineValue "New applications found"
#              $lastWrite = (get-item $file).LastWriteTime
#              $timespan = new-timespan -days 0 -hours 4 -minutes 5
#
#                if (((get-date) - $lastWrite) -gt $timespan) {
#                    Log-Write -LogPath $logfile -LineValue "File is older than 4 h, will reset"
#
#                         try {
#                            Log-Write -LogPath $logfile -LineValue "Remove permanent list"
#                            Remove-Item $file
#                          }
#                          catch {
#                                  Remove-Item $temp
#                                  Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
#                          }
#
#                          try {
#                                  Log-Write -LogPath $logfile -LineValue "Rename temporary list"
#                                  Rename-Item $temp "$prefix apps.xml"
#                                  }
#                          catch {
#                                   Log-Error -LogPath $logfile -ErrorDesc "Unable to switch temp-list to permanent" -ExitGracefully $false
#                                }
#                }
#                Else {
$newapps = $true
#                }

}

}
If ($newapps -eq $true) {
return $True

}
#Remove-Variable * -ErrorAction 'SilentlyContinue'
#$error.Clear()
#Clear-Host
#$diffs = $null
#$diffsserver = $null
}
}

function OnApplicationLoad {
#Note: This function is not called in Projects
#Note: This function runs before the form is created
#Note: To get the script directory in the Packager use: Split-Path $hostinvocation.MyCommand.path
#Note: To get the console output in the Packager (Windows Mode) use: $ConsoleOutput (Type: System.Collections.ArrayList)
#Important: Form controls cannot be accessed in this function
#TODO: Add snapins and custom code to validate the application load



return $true #return true for success or false for failure
}

function Get-NewAppCatalogApps {

[CmdletBinding()]
param
(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Permanent-file')]
$file,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Temp-file')]
$temp,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Path to logfile')]
$logfile
)

Process {


try {
$array = @(Compare-Object $(Get-content $file -ReadCount 0) $(Get-Content $temp -ReadCount 0) | Where {$_.SideIndicator -eq '=>' -and $_.InputObject -match '<S N="Name">'} |Select-Object $_ -ExpandProperty InputObject)
}
catch {
Log-Error -LogPath $logfile -ErrorDesc "Unable to list new applications" -ExitGracefully $false
return
}

[array]$exclusionlist = Get-ExclusionList
Log-Write -LogPath $logfilePath -LineValue "Exclusions:"
Log-Write -LogPath $logfilePath -LineValue "$($exclusionlist)"
$intApps = $array.Length - 4
Log-Write -LogPath $logfilePath -LineValue "User has $(4+$intApps) new applications"
$i = 0
$applist = ""
foreach ($element in $Array) {

$element = $element.TrimStart(' ')
$element = $element -replace "</S>","`n"
$element = $element -replace "<S N=`"Name`">",""

if ($exclusionlist -contains $($element -replace "`n",""))
{

Log-Write -LogPath $logfilePath -LineValue "$($element -replace   `"`n`",`"`") is now excluded"
$array.remove($element)
$intApps-=1
}
else
{
$i++

if ($i -lt "5")
{

#$element = $element.TrimStart(' ')
#$element = $element.TrimEnd('</S>')
#$element = $element -replace "</S>","`n"
#$element = $element -replace "<S N=`"Name`">",""
$applist += $element

#$applist = $applist.TrimStart(' ')
}
else {
$applist = $applist + "and $intApps more`n"
return $applist
}
}
}
return $applist
}

}

function OnApplicationExit {
#Note: This function is not called in Projects
#Note: This function runs after the form is closed
#TODO: Add custom code to clean up and unload snapins when the application exits
#Log-Finish -LogPath $logfilePath -NoExit $true
$script:ExitCode = 0 #Set the exit code for the Packager
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

$AppNotify_Load={
#TODO: Initialize Form Controls here
$NotifyIcon.Text = $list
#$NotifyIcon.BalloonTipText = $list
$NotifyIcon.ShowBalloonTip(30000,"New Applications Available",$list, 'Info')
}

#region Control Helper Functions
function Show-NotifyIcon
{
<#
.SYNOPSIS
Displays a NotifyIcon's balloon tip message in the taskbar's notification area.

.DESCRIPTION
Displays a NotifyIcon's a balloon tip message in the taskbar's notification area.

.PARAMETER NotifyIcon
The NotifyIcon control that will be displayed.

.PARAMETER BalloonTipText
Sets the text to display in the balloon tip.

.PARAMETER BalloonTipTitle
Sets the Title to display in the balloon tip.

.PARAMETER BalloonTipIcon
The icon to display in the ballon tip.

.PARAMETER Timeout
The time the ToolTip Balloon will remain visible in milliseconds.
Default: 0 - Uses windows default.
#>
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNull()]
[System.Windows.Forms.NotifyIcon]$NotifyIcon,
[Parameter(Mandatory = $true, Position = 1)]
[ValidateNotNullOrEmpty()]
[String]$BalloonTipText,
[Parameter(Position = 2)]
[String]$BalloonTipTitle = '',
[Parameter(Position = 3)]
[System.Windows.Forms.ToolTipIcon]$BalloonTipIcon = 'None',
[Parameter(Position = 4)]
[int]$Timeout = 0
)

if($NotifyIcon.Icon -eq $null)
{
#Set a Default Icon otherwise the balloon will not show
$NotifyIcon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon([System.Windows.Forms.Application]::ExecutablePath)
}

$NotifyIcon.ShowBalloonTip($Timeout, $BalloonTipTitle, $BalloonTipText, $BalloonTipIcon)
}

#endregion

$NotifyIcon_MouseDoubleClick=[System.Windows.Forms.MouseEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.MouseEventArgs]
#TODO: Place custom script here
Log-Write -LogPath $logfilePath -LineValue "User clicked icon"
Log-Write -LogPath $logfilePath -LineValue "Sending user to $appcatalog"
Start-Process $appcatalog

Log-Write -LogPath $logfilePath -LineValue "Timer started for $totaltime"
#Add TotalTime to current time
$script:StartTime = (Get-Date).AddSeconds($TotalTime)
#Start the timer
$timer1.Start()

}

$NotifyIcon_MouseClick=[System.Windows.Forms.MouseEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.MouseEventArgs]
#$NotifyIcon.Visible = $true
$NotifyIcon.ShowBalloonTip(30000,"New Applications Available",$list, 'Info')
}

$NotifyIcon_BalloonTipClicked={
Log-Write -LogPath $logfilePath -LineValue "User clicked ballontip"
Log-Write -LogPath $logfilePath -LineValue "Sending user to $appcatalog"
Start-Process $appcatalog

Log-Write -LogPath $logfilePath -LineValue "Timer started for $totaltime"
#Add TotalTime to current time
$script:StartTime = (Get-Date).AddSeconds($TotalTime)
#Start the timer
$timer1.Start()

}

#Get path which scripts run from
$CurrentPath = Get-ScriptDirectory

#Import log-functions
#. "$CurrentPath\Logging_Functions.ps1"

&nbsp;

#Prefix for all generated files in user's %TEMP%
$prefix = "appsnotify"

#Logfile
$logfilePath = $env:temp+"\$prefix app.log"
#Visibility after icon-click
$TotalTime = 300
$TotalTime_All = 14300

if ((Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" | Select-Object -ExpandProperty FreeSpace) -lt "20000000" ) {
exit
}

$check=Get-Process AppsNotify -ErrorAction SilentlyContinue | Measure-Object

if ($check.count -lt "2") {

}
else {
Log-Error -LogPath $logfilePath -ErrorDesc "AppsNotify is already running. Terminating. " -ExitGraceFully $false
exit
}

#Temporary file to store applications
$tempfilePath  = $env:temp+"\$prefix app_temp.xml"
#Permanent file to store applications
$filePath = $env:temp +"\$prefix apps.xml"
#Reset log-file for this session
Remove-Item $logfilePath

&nbsp;

################################################################################################################
Log-Start -LogPath $env:temp -LogName "$prefix app.log" -ScriptVersion "2.0"

&nbsp;

#Verify that the $CommandLine variable exists
if($CommandLine -ne $null -and $CommandLine -ne "")
{
#Log-Write -LogPath $logfilePath -LineValue "There is a command-line"
Log-Write -LogPath $logfilePath -LineValue "Command-line is:"
Log-Write -LogPath $logfilePath -LineValue "$CommandLine"
#$Arguments = Parse-Commandline $CommandLine
#Convert the Arguments. Use – as the Argument Indicator
$Dictionary = New-Object System.Collections.Specialized.StringDictionary
Convert-CommandLineToDictionary -Dictionary $Dictionary -CommandLine $Commandline  -ParamIndicator '-'
}
else
{
#Not running in a packager or no command line arguments passed
Log-Error -LogPath $logfilePath -ErrorDesc "No command-line argument. Use -appcatalog <url>" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

$appcatalog = $Dictionary["appcatalog"]
if($appcatalog -ne $null -and $appcatalog -ne "") {
Log-Write -LogPath $logfilePath -LineValue "Passed Application Catalog is $appcatalog"
if (Validate-IsURL -Url $appcatalog) {
Log-Write -LogPath $logfilePath -LineValue "Passed Application Catalog is a URL"
}
Else {
Log-Error -LogPath $logfilePath -ErrorDesc "This is not a url" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

}
else {
#Address to Application Catalogue
Log-Error -LogPath $logfilePath -ErrorDesc "We need an Application Catalog" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

if ($(([PInvoke.Win32.UserInput]::IdleTime).TotalMinutes) -gt 10) {
Log-Write -LogPath $logfilePath -LineValue "Idle Time: $(([PInvoke.Win32.UserInput]::IdleTime).TotalMinutes)"
Log-Write -LogPath $logfilePath -LineValue "No user at computer, exiting"
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

if ((Get-CMUserApps -url $appcatalog -logfile $logfilePath -temp $tempfilePath) -eq $true) {

if ((Compare-CMUserApps -file $filePath -temp $tempfilePath -logfile $logfilePath) -eq $true) {
$list = "$(Get-NewAppCatalogApps -file $filePath -temp $tempfilePath -logfile $logfilePath)"
if ($list)
{
Log-Write -LogPath $logfilePath -LineValue "Applist is $list"
#$NotifyIcon.Text = $list
try {

$NotifyIcon.Visible = $true
#Add TotalTime to current time
Log-Write -LogPath $logfilePath -LineValue "Starting general timer..."
$script:StartTime_all = (Get-Date).AddSeconds($TotalTime_All)
#Start the timer
$timer_all.Start()

}
catch {
Log-Error -LogPath $logfilePath -ErrorDesc "Tray icon failed..." -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}
finally {
try {
Log-Write -LogPath $logfilePath -LineValue "Removing $filepath"
Remove-Item $filePath
Log-Write -LogPath $logfilePath -LineValue "Renaming $tempfilePath"
Rename-Item -Path "$tempfilePath"  -NewName "$prefix apps.xml" -Force

}
catch {
Remove-Item $tempfilePath
Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
}
}
}
else {
try {
Log-Write -LogPath $logfilePath -LineValue "Removing $filepath"
Remove-Item $filePath
Log-Write -LogPath $logfilePath -LineValue "Renaming $tempfilePath"
Rename-Item -Path "$tempfilePath"  -NewName "$prefix apps.xml" -Force

}
catch {
Remove-Item $tempfilePath
Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
}
Rename-Item -Path "$tempfilePath"  -NewName "$prefix apps.xml" -Force
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

}
Else {
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

}
Else {
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

&nbsp;

&nbsp;

$NotifyIcon_BalloonTipShown={
#TODO: Place custom script here
Log-Write -LogPath $logfilePath -LineValue "Notifying user"
}

$timer1_Tick={
#Use Get-Date for Time Accuracy
[TimeSpan]$span = $script:StartTime - (Get-Date)

#Update the display
#$formSampleTimer.Text = $labelTime.Text = "{0:N0}" -f $span.TotalSeconds

if($span.TotalSeconds -le 0)
{
Log-Write -LogPath $logfilePath -LineValue "Timer has passed"
$timer1.Stop()
$NotifyIcon.Visible = $false
$AppNotify.Close()
$NotifyIcon.Dispose()
Log-Finish -LogPath $logfilePath -NoExit $true
}
}

$timer_all_Tick={
#TODO: Place custom script here
#Use Get-Date for Time Accuracy
[TimeSpan]$span = $script:StartTime_all - (Get-Date)

#Update the display
#$formSampleTimer.Text = $labelTime.Text = "{0:N0}" -f $span.TotalSeconds

if($span.TotalSeconds -le 0)
{
Log-Write -LogPath $logfilePath -LineValue "General timer is up.. closing..."
$timer_all.Stop()
$NotifyIcon.Visible = $false
$AppNotify.Close()
$NotifyIcon.Dispose()
Log-Finish -LogPath $logfilePath -NoExit $true
}
}

CM, IP-ranges and unknown networks

A follow-up of a previous post relating to matching ConfigMgr IP-range boundaries to known networks. The essence is to send an email (scheduled at your own interval) that notifies if there are any clients on unknown networks inventoried.

Prerequisites

  • Configuration Manager is required to have IP-ranges for boundaries
  • We assume that boundaries are /24, or 255.255.255.0 and IPv4
  • Clients need to collect and report Network Data
  • The user running the script needs to be able to connect and read data from Configuration Manager database
  • SMTP-server to send an email

What do we do?

  • Gather all active IP-range boundaries from database
  • Gather reported networks that matches the DNS-Suffix defined, IPv4 and sum up # of devices within /24-networks

Loads of assumptions….  incase you need to tweak it this is how we gather from the database.

Client networks – alter for $netquery. Remember to replace $dnsdomain

select 
SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) + '.1' As IP,
COUNT(*) as Devices
 from v_Network_DATA_Serialized as ip 
where ip.IPAddress0 IS NOT NULL and 
ip.IPSubnet0 != '64'  and 
ip.IPSubnet0 != '128' and 
ip.IPSubnet0 = '255.255.255.0' and 
ip.DNSDomain0 IS NOT NULL and 
ip.DNSDomain0 = '$dnsdomain' and 
ip.TimeStamp > DATEADD(day, -10, GETDATE()) 
GROUP BY  SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) 
ORDER BY Devices DESC

Boundaries – alter for $query

select 
bound.DisplayName, 
SUBSTRING(bound.value,1,CHARINDEX('-',bound.value) -1) AS LEFTHALF,
SUBSTRING(bound.value,CHARINDEX('-',bound.value) +1 ,100) AS RIGHTHALF 
from vSMS_Boundary as bound 
where 
bound.BoundaryType = '3' and 
bound.GroupCount > 0

Output – email

An email is sent with the following information after running the script

Network – network (always ends with a .1)
Devices – number of devices
DNSDomain – DNS-suffix
IPSubnet – network mask
DefaultGateway – default gateway
DHCPServer – DHCP-server

Parameters

Before running the actual script the following is required to be updated. DNSDomain – what DNS-suffix your clients are reporting as.

#Database params
$ErrorActionPreference = "silentlycontinue"
#Database-server
$datasource = "DBSERVER"
#Database
$database = "CM_DATABASE"
#DNS-Domain
$dnsdomain = 'dns.suffix.se'

#Email params
$EmailParams = @{
To         = 'email'
From       = 'email'
Smtpserver = 'smtp.company.se'
Subject    = "ConfigMgr Client Unknown Networks -  $(Get-Date -Format dd-MMM-yyyy)"
}

Script

#========================================================================
# Created with: Powershell ISE
# Created on:   2017-08-13
# Created by:   NiKa
# Organization:
# Filename:     CM_BoundaryCheck.ps1
#========================================================================

function IsIpAddressInRange {
param(
[string] $ipAddress,
[string] $fromAddress,
[string] $toAddress
)

$ip = [system.net.ipaddress]::Parse($ipAddress).GetAddressBytes()
[array]::Reverse($ip)
$ip = [system.BitConverter]::ToUInt32($ip, 0)

$from = [system.net.ipaddress]::Parse($fromAddress).GetAddressBytes()
[array]::Reverse($from)
$from = [system.BitConverter]::ToUInt32($from, 0)

$to = [system.net.ipaddress]::Parse($toAddress).GetAddressBytes()
[array]::Reverse($to)
$to = [system.BitConverter]::ToUInt32($to, 0)

$from -le $ip -and $ip -le $to
}

###### Parameters #################

#Database params
$ErrorActionPreference = "silentlycontinue"
#Database-server
$datasource = "DBSERVER"
#Database
$database = "CM_DATABASE"
#DNS-Domain
$dnsdomain = 'dns.suffix.se'

#Email params
$EmailParams = @{
To         = 'email'
From       = 'email'
Smtpserver = 'smtp.company.se'
Subject    = "ConfigMgr Client Unknown Networks -  $(Get-Date -Format dd-MMM-yyyy)"
}
###### Parameters #################

#### Retrieve client networks
$netquery = "select SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) + '.1' As IP, COUNT(*) as Devices from v_Network_DATA_Serialized as ip where ip.IPAddress0 IS NOT NULL and ip.IPSubnet0 != '64'  and ip.IPSubnet0 != '128' and ip.IPSubnet0 = '255.255.255.0' and ip.DNSDomain0 IS NOT NULL and ip.DNSDomain0 = '$dnsdomain' and ip.TimeStamp > DATEADD(day, -10, GETDATE()) GROUP BY  SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) ORDER BY Devices DESC"

$networks= Invoke-Sqlcmd -Query $netquery -server $datasource -Database $database

#### Retrieve boundaries
$query = "select bound.DisplayName, SUBSTRING(bound.value,1,CHARINDEX('-',bound.value) -1) AS LEFTHALF,SUBSTRING(bound.value,CHARINDEX('-',bound.value) +1 ,100) AS RIGHTHALF from vSMS_Boundary as bound where bound.BoundaryType = '3' and bound.GroupCount > 0"

$iprange = Invoke-Sqlcmd -Query $query -server $datasource -Database $database

#### Check if IP-address are within boundaries
$report=@()
foreach ($net in $networks) {
if (!($net.ip -eq '192.168.1.1' -or $net.ip -eq '0.0.0.1'  -or $net.ip -eq '10.10.0.1' -or $net.ip -eq '172.16.0.1' -or $net.ip -eq '169.254.43.1' -or $net.ip -eq '169.254.36.1' -or $net.ip -eq '192.168.0.1' -or $net.ip -eq '10.0.100.1')) {
$i = 0
$J = $iprange.count
$boundaryfound = $false
do {
#$iprange[$i].displayname
if (IsIpAddressInRange $net.ip $iprange[$i].LEFTHALF $iprange[$i].RIGHTHALF)
{

$boundaryfound = $true
}
$i++
} until ($i -gt $j)
if ($boundaryfound -eq $false)
{
#write-host "Network: $($net.ip) - Devices: $($net.Devices)"

#Retrieve information about network
$devquery = "select distinct DNSDomain0,IPSubnet0,DefaultIPGateway0,DHCPServer0 from v_Network_DATA_Serialized as ip
where ip.IPAddress0 IS NOT NULL
and ip.IPSubnet0 != '64'
and ip.TimeStamp > DATEADD(day, -10, GETDATE())
and ip.IPaddress0 like '$($($net.ip) -replace ".$")%'"
$devices= Invoke-Sqlcmd -Query $devquery -server $datasource -Database $database
$report += New-Object psobject -Property @{Network=$($net.ip);Devices=$($net.devices);DNSDomain=$($devices.DNSDomain0);IPSubnet=$($devices.IPSubnet0);DefaultGateway=$($devices.DefaultIPGateway0);DHCPServer=$($devices.DHCPServer0)}
}
}
}

if ($report -ne $null) {

#$report

#Generate email

$style = @"
<style>
body {
color:#333333;
font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;}
}
h1 {
text-align:center;
}
h2 {
border-top:1px solid #666666;
}
table {
border-collapse: collapse;
font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;
}
th {
font-size: 10pt;
text-align: left;
padding-top: 5px;
padding-bottom: 4px;
background-color: #1FE093;
color: #ffffff;
}
td {
font-size: 8pt;
border: 1px solid #1FE093;
padding: 3px 7px 2px 7px;
}
</style>

"@

$Properties = @(
'Network',
'Devices',
'DNSDomain',
'IPSubnet',
'DefaultGateway',
'DHCPServer'
)

$body = $report |
Select-Object -Property $Properties|
ConvertTo-Html -Head $style -Body "
<H3>Devices from unknown networks ($($results.Count))</H3>

" |
Out-String

Send-MailMessage @EmailParams -Body $Body -BodyAsHtml

}

Inventory % for App-V and ARP

A SQL query that needs to run against the Configuration Manager database and present all software that is installed for a specific collection (App-V or traditionally registered software in Programs and Features)

Will present;

Software Name
Software Version
App-V or ProductCode
# installations
% av devices that have the software installed (in comparision to collection members)
# Collection Members

 

DECLARE @collection VARCHAR(50),
@DisplayName VARCHAR(50);

SET @collection = 'SS100517';
set @DisplayName = 'Adobe Audition%'

select arp.DisplayName0 as 'Name', arp.Version0 as 'Version', arp.ProdID0 as 'GlobalorProd', COUNT(*) as Total, count(distinct arp.resourceid) * 100/
(
SELECT count(distinct ws.resourceid)
from v_ClientCollectionMembers ws
where ws.CollectionID = @collection
) as '%',
(
SELECT count(distinct ws.resourceid)
from v_ClientCollectionMembers ws
where ws.CollectionID = @collection
) as 'Collection Members'
from v_Add_Remove_Programs as arp
inner join v_ClientCollectionMembers as coll on arp.ResourceID = coll.ResourceID
where arp.DisplayName0 LIKE @DisplayName
and  CollectionID = @collection
GROUP BY arp.DisplayName0, arp.Version0, arp.ProdID0
UNION
select appv.Name00 as 'Name', appv.Version00 as 'Version', 'AppV' as 'GlobalorProd', COUNT(*) as Total,
count(distinct appv.machineid) * 100/
(
SELECT count(distinct ws.resourceid)
from v_ClientCollectionMembers ws
where ws.CollectionID = @collection
) as '%',
(
SELECT count(distinct ws.resourceid)
from v_ClientCollectionMembers ws
where ws.CollectionID = @collection
) as 'Collection Members'

from APPV_CLIENT_PACKAGE_DATA as appv
inner join v_ClientCollectionMembers as coll on appv.MachineID = coll.ResourceID
where appv.Name00 LIKE @DisplayName
and  CollectionID = @collection
GROUP BY appv.Name00, appv.Version00, appv.PackageId00

Device, primary user and model

A simple query that will present the following information based on what ConfigMgr has inventoried

Computer-name
Primary User(Domain\username)
Manufacturer
Model

SQL;

select umr.MachineResourceName as 'Device name', umr.UniqueUserName as 'Username',
CASE
when CAST(umsr.SourceID as varchar(20))  = '1' then 'Software Catalog'
when CAST(umsr.SourceID  as varchar(20)) =  '2' then 'Administrator'
when CAST(umsr.SourceID as varchar(20)) =  '3'  then 'User'
when CAST(umsr.SourceID as varchar(20)) =  '4'  then 'Usage agent'
when CAST(umsr.SourceID as varchar(20)) =  '5'  then 'Device Management'
when CAST(umsr.SourceID as varchar(20)) =  '6'  then 'OSD'
when CAST(umsr.SourceID as varchar(20)) =  '7'  then 'Fast Install'
else CAST(umsr.SourceID as varchar(20)) END as 'Source',
cs.Manufacturer0 AS 'Manufacturer',
CASE
when cs.Manufacturer0 = 'Lenovo' Then hwdata.Version0
else cs.Model0
END as Model
from v_UserMachineRelationship as umr
inner join v_UserMachineSourceRelation as umsr on umr.RelationshipResourceID = umsr.RelationshipResourceID
inner JOIN v_GS_COMPUTER_SYSTEM AS cs ON umr.MachineResourceID = cs.ResourceID
left join v_GS_COMPUTER_SYSTEM_PRODUCT as hwdata on umr.MachineResourceID = hwdata.resourceid

Windows 7 x64 and TPM 2.0

If you are using Bitlocker, Configuration Manager 1610 or higher and get a piece of hardware that has TPM 2.0 there are a few more hurdles to get the device encrypted. Coretech has written notes from the field which states the two options going forward. Following their recommendation – lets go down the path of running a device with CSM (UEFI) enabled and installing Windows 7.

Make sure that the device is running in UEFI and is setup as expected. I will leave the actual configuration of BIOS / UEFI out of this – as any device with TPM 2.0 will most likely have a preconfigured settings that are optimal. If not, time to configure for UEFI (without Secure Boot).

UEFI Boot

Once that is completed – create a new group that has the intention to identify if we (if possible) booted with a legacy option (legacy PXE-boot, legacy USB stick etc etc). Validate that its not UEFI-booted and that we have a TPM 2.0 chip (and add a check that we are also intending to install Windows 7 x64).

image

Only two steps are required in the group – format the drive as GPT and then a restart to the boot-image. Suggested format:

image

Preprovision

Once we have ensured that the device is running in UEFI, and not legacy…, and since we all are running a newer ADK then 1511 – we should add three registry keys per Microsoft

Above configuration of the algorithms for Windows 10 (build 1511) is kept as REG_DWORDs under:

HKLM\SOFTWARE\Policies\Microsoft\FVE

Operating System drives: EncryptionMethodWithXtsOs

Fixed Data drives: EncryptionMethodWithXtsFdv

Removable Data drives: EncryptionMethodWithXtsRdv

Hotfix

You can integrate the TPM 2.0 hotfix into the Windows 7, or simply install it after the WIM has been copied onto the machine. It should be installed before MBAM (or just bitlocker) is installed and starts handling the encryption

Task Sequence and shutdown (not reboot) a computer and continue

For some reason there is a requirement to do a computer shutdown (not restart) while running a task sequence, and once the computer starts again there is a need to continue running the task sequence where we left it.

How do you go about that? Let’st start…

image

We need two scripts, a task sequence with the ability to run one script and then to start a task sequence controlled restart.

For testing purposes a networkshare was used instead of leveraging a package, but in real-life and in production – all of the files can be placed in a package and executed from there.
This concept is tested within WinPE (using Winpeutil etc…), but you can most likely adapt it to a Windows installation.

Run Monitor

The ‘Run Monitor’ step will kick off a VB-Script that will start a powershell script – and then exit. Simple enough to start a script, and then allow the task sequence to continue with the next steps

image

VBScript
Runapp "powershell.exe","-noprofile -executionpolicy bypass -file " & GetScriptPatH() & "shutdown.ps1"

Private Function RunApp(AppPath,Switches)
Dim WShell
Dim RunString
Dim RetVal
Dim Success

On Error Resume Next

Set WShell=CreateObject("WScript.Shell")

RunString=Chr(34) &AppPath & Chr(34) & " " & Switches
Retval=WShell.Run(RunString,0,False)

RunApp=Retval

Set WShell=Nothing
End Function

Private Function GetScriptPath
GetScriptPath=Replace(WScript.ScriptFullName,WScript.ScriptName,"")
End Function

The powershell-script (shutdown.ps1) looks as follows;

  1. Create a TS Environment (so we can read variables)
  2. Verify if the variable _SMSTSBootStagePath is set
  3. If the drive-part is longer than a single-letter – we know that the boot-image is prepared and that the reboot countdown has started.
Powershell
$end =$true
write-output "start"

DO
{
start-sleep 2
Get-date
#Remove-Variable -name tsenv -Force -ErrorAction SilentlyContinue
if (!$tsenv) {
try  {
$tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment
}
catch {
write-output "No TS started yet"
}
}
try  {
$bootpath = $tsenv.Value("_SMSTSBootStagePath") -split ":"
$tsenv.Value("_SMSTSBootStagePath")
if ($bootpath[0].length -gt 1) {
write-output "SMSTSBootStagePath prepped for reboot"
$end = $false
}
}
catch {
write-output "variable not set"
}

} While ($end -eq $true)

start-sleep 5

wpeutil shutdown

Restart

The restart step is fairly generic and you can configure it as you need. A thing to note is that the time-out needs to be higher than the start-sleep within the Powershell-script. As the purpose is to continue within WinPE – the step is configured to start to the boot-image.

image

ConfigMgr and “Who updated All Systems–again”?

As this questions shows up at all places for the same reason; Someone pressed the “Updated Membership” on a collection way up in hierarchy of collections. A simple right-click and press – and then accept.

image

The action is simple, however if the collection is ‘far up’ in the hierarchy and have all (or many) collections indirectly connected to it also start to update – an unforgiven workload. Delays, frustration and mayhem will ensue (perhaps a bit over the top here..)

If you are a type of organisation that blames people – well, here you go!

image

Choose to create a new Status Message Query and use the following query to filter the search based on time and only look for specific the refresh action of collections

Query;

select stat.*, ins.*, att1.*, stat.Time 
from SMS_StatusMessage as stat 
left join SMS_StatMsgInsStrings as ins on stat.RecordID = ins.RecordID 
left join SMS_StatMsgAttributes as att1 on stat.RecordID = att1.RecordID 
where stat.MessageType = 768 and 
stat.MessageID = 30104 and 
stat.Time >= ##PRM:SMS_StatusMessage.Time## 
order by stat.Time desc

image

End result – with the user account and collection to blame;

image

ConfigMgr, SQL Availability Groups and upgrades

If leveraging the new great features from SQL Server the last years – certainly the Availability Group have been a tempting way to increase availability for ConfigMgr (despite the fact that everyone prefers the local SQL Server running on the Primary Site server).  Now, as ConfigMgr TechPreview 1706 announced Site Server Role High Availability – separating the site server and the SQL database might be a new preferred method going forward in the future.

Microsoft provided a great step-by-step (2016-05-14) approach on all the steps required to migrate an existing database to a SQL Server Availability Group. As part of this a few specific SQL configurations are stated as a way to ensure an operational database;

We need to Set Trustworthy and enable CLR Integration. Run this against Primary Replica (You can confirm from the AlwaysOn dashboard)

Reviewing the official documentation (2017-05-26) there are however a few additional configurations detailed;

  • CLR Integration must be enabled
  • Max text repl size must be 2147483647
  • The database owner must be the SA account
  • TRUSTWORTY must be ON
  • Service Broker must be enabled

As a great benefit to that article there is a check for all the configuration options that clearly states wether they are correct or not. All of the above, but one, are very much a requirement for basic functionality. The Max text repl size was not stated in the step-by-step guide and is not a requirement for a functional database for normal operations, however during an upgrade it will be immediately detected as a deviation. What does ConfigMgr do with deviations? Well, it tries to fix it of course (don’t mention inbox backlogs or… or…).

An upgrade with settings configured according to the above requirements

image

An upgrade where on setting deviates from the requirements and the SQL database is part of an availability group;

image

The operation cannot be performed on database [CM_P01] because it is invovled in a database mirroring…
ALTER DATABASE [CM_P01] SET SINGLE_USER WITH ROLLBACK IMMEDIATE
Gives the response: Failed to set database [CM_P01] to SINGLE_USER mode

As ConfigMgr tries to fix the deviation it also tries to set the ConfigMgr database in SINGLE_USER mode, unfortunately that is not possible as long as the database is part of the Availability Group and mirrored. Only way is to configure according to the Microsoft recommend settings outside of ConfigMgr and then retry the upgrade.