Apple iTunes 12.7 and Software License Agreement registry key

Image result for itunesiTunes is the ugly step-child (or the Meg, or the bastard) of the Apple family. As iPhone still remains the phone of choice within my family and quite a few corporations there is still a need from time to time to deal with this excuse of a software.

As a revisit to the previous post where I did track down howto eliminate the end-user requirement to accept the Software License Agreement on corporate installations – there has been some changes in the years past. As the software has decided the progress updates are evil and show no indication of pretty much anything moving this will be how time is spent to track the latest version.

Previously to identify the necessary parts to avoid presenting the SLA for the end-user we required two parts. The first is an identifier for the SLA, and the second was where to put this identifier in the registry.

Registry

Second things first. The registry key still remains the same. The below is the registry key necessary for a Windows 10 x64 installation of Apple iTunes 12.7.

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Apple Computer, Inc.\iTunes]
"SLA"="EA1511"

 

SLA value

To retrieve the value (EA1511) we need a slightly different process than previously used. The value was stored in iTunesPrefs.xml in the earlier versions of iTunes, however that file doesn’t exist anymore and instead we can see the following files

image

Opening all of them will reveal binary or hexadecimal-files and neither will allow us to decipher anything we need. As the SLA is most likely located within the installation folder we can poke around and see if the actual SLA will provide us with anything.

Locating the RTF-file License within en.lproj (for english users) could potentially contain something useful.

image

Opening the document and scrolling around it will reveal the two last lines at the bottom of the document

image

And there it is!

End result

image

ConfigMgr – Howto generate an inbox-backlog

Ever wondered how someone can generate an enormous inbox-backlog for a Configuration Manager site? Apart from not scaling the environment correctly based on the performance of the infrastructure and the number of clients and features its servicing – here are a two methods to bring pretty much everything to a halt.

Server Groups

Server Groups is a feature not yet released for production. It became available in ConfigMgr 1606 and has been worked on since. As one can check it is required to be enabled from Updates and Servicing under Administration in the Feature list, however its been toggled ‘On’ in a number of sites where noone has actively thought about using this feature. Perhaps its toggled on per default?

image

There are a great number of write-ups by independt specialists aswell as an extensive Microsoft documentation.

The Server Group is defined per collection, and there are a few safeguards in place to avoid harm – for example you can’t se this on the generic ConfigMgr collections – such as the All systems

image

Any other collection, regardless of how many members it has, can toggle this on.

image

Locating these collections that are enabled for Server Groups is fairly easy by simplying recursively searching for all collections that has Server Groups to Yes – check the Server Group search criteria.

image

image

Create the backlog

The intent is to control Software Updates and in what order they are applied. If Server Group is enabled for a collection with all (or many devices) and these devices have deadline for a Software Update set – things will happen directly after these deadlines. Each client will effectively send a status update regarding patching status roughly once every 5 minutes (or each minute?)

Now, watch that backlog go…

Relationships – Dependency or Supersedence

Deeply burried with a side-note on howto create Dependencies within a Configuration Manager application is the following limit specified;

In some cases, a deployment type is dependent on a deployment type that also contains dependencies. In this scenario, where a chain of dependencies exists, the maximum number of supported dependencies in the chain is five.
Source: Technet¨

After effective testing the following is concluded – this isn’t a hard-limit that measures the limit of 5 level deep dependency chain – but rather a complete halt of so many things when its to  ‘complex’ and ‘to large’. Or – its not the exact number of 5 that is the limit but rather that depending on loads of factors (complexity of application object, how many, how deep etc)  this will eventually generate a failure of the handling within the Configuration Client or simply to many status messages sent. Also – the limit is very applicable for supersedence.

A chain of 5 dependencies or supersedences is a very good estimate for when things will function, and beyond that you are at the mercy of luck and testing to see if things work.

What adds payload and therefore risk of failure?

  • Requirements
  • Detections
  • Conditions
  • Dependencies
  • Many levels of dependency
  • Supersedence
  • Many levels of supersedence

The more you have of the above the more has to be evaluated and processed. As a consultant I have learned the hardway to avoid using the ‘always’, ‘never’ etc. Usually time is saved if dependencies and supersedence is limited in use. Wrap it in whatever preferred scripting language you have.

Now, if you have neatly packaged and deployed all versions of Java or Adobe for the last 2-3 years – there should be quite a few versions available within ConfigMgr. Using supersedence this is a great way to start generating that backlog;

Create the backlog

Create the latest version and ensure you replace about 8 previous versions in the first level. After the first level there should be roughly 7 deeper levels for each one in the first level. Something in the lines of

image

Aim to deploy the application to any collection that contains many devices and watch that backlog start creeping up…

Why create this havoc?

The intent is not to teach novice users howto wreak havoc within an environment, but rather what potentially could cause a backlog in the ConfigMgr processing and howto identify ill advised practices within a company.

 

Windows 10–1709 and OneDrive UX

OneDrive receives some well deserved attention in every upgrade of Windows 10. The road to an exciting user experience is paved with some hiccups, but once 1709 came around – most of the quirks were sorted out.

OneDrive is still seen in the system tray as two clouds (blue for OneDrive for Business and white for the personal edition). Spotting them in the task manager reveals a different confusion still.

image

image

One of the advocated new improvements is the ability to maintain sparse-files, or pointers, or – well, the point beeing; the file is not on disk and will only be retrieved from the cloud when needed (or requrested). The ability needs toggling under the Settings for each cloud (business or personal) and named Files On-Demand (or this can be enabled via group policy per machine)

image

Technically Files On-Demand is an attribute set for each file. The state is called Pinned or Unpinned and can be toggled via the “attrib” command. No idea why they chose to maintain an older command rather than enabling this via Powershell only.
To make files available offline; attrib -U +P /s

image

The end-user isn’t required to know this and can toggle the options from  right-click option when selecting any file or folder. The names aren’t the same, but rather offer a more end-user friendly name.

image

Applying the Free up space option will clearly show the progress of altering the attribute by saying Applying properties….

image

As a way to educate the user and offer a clear view of the state of any folder (offline, online, issues or syncing) there is a new column added (as opposed to the previous overlay on the icons) named Status. Personally this seems to be an improvement (allows for sorting for example).

image

Remember the system tray icons? A bit more useful this time around – if you left-click them once the following status will be shown.

OneDrive for Business

image

OneDrive

image

The reason for this write-up is based on the experience of migrating all OneDrive content to OneDrive for Business. Odd thing once I was completed. (1) is the OneDrive for Business and (2) is OneDrive. For some reason the consumption of data differs with just above 100gb. Where did this go? The below is from settings of the OneDrive-client.

image

Comparing the folder on disk looks like this shows that both are roughly 237gb – so this seems odd. Verifying online via the Manage Storage button shows some correct numbers and verifiying basic folder structure from the web-interface provides some additional confidence. Accessing the OneDrive for Business via the admin-center for Office 365 and spinning up the Reports for usage – shows that 8mb is currently consumed?

Are there any more ways to confirm how much data I got?

image

As OneDrive for Business is essentially a glorified interface for Sharepoint – there are certain limitations defined. There are restrictions on files both in OneDrive and OneDrive for Business, however OneDrive for Business has far more annoying limitations. Sample output to resolve the 181 conflicts – a number far lower than expected to be honest.

image

Get-FileInfo

Had to retrieve information if a filed was locked and who owned it – so wrote the below Powershell function. Ways to improve would perhaps be to provide parsing of multiple-files.

<?Function Get-FileInfo {
<#
.SYNOPSIS
Retrieves file-information, such as size, name and locks
.DESCRIPTION
Outputs an object with Path, Size, Created on, Last Write Time,
Owner and if the file is locked
.EXAMPLE
Get-FileInfo -File c:\windows\regedit.exe
#>
[CmdletBinding()]
param(
[Parameter(mandatory=$true)]
[string]$File
)
Begin
{
write-verbose "------------------------"
write-verbose "Start of Get-FileInfo"
write-verbose "Computername:  $($env:computername)"
write-verbose "Username: $($env:USERNAME)"
Write-verbose "Validate file $($file)"

if (Test-Path  $($file))
{
Write-Verbose "File exists"
}
else
{
throw-error "File does not exist"
}

}
Process
{

#Retrieve file object
$objfile = Get-ChildItem $file

#check file lock
try { [IO.File]::OpenWrite($objfile).close();$lock = $false }
catch {$lock = $true}

#output object
New-Object PSObject -Property @{
Path = $objFile.fullname
Size = "{0:N2} MB" -f ( $objFile.Length / 1mb )
'Created on' = $objFile.CreationTime
'Last Write Time' = $objFile.LastWriteTime
Owner = (Get-Acl $File).Owner
Lock = $lock

}

}
End
{
write-verbose "End of Get-FileInfo"
write-verbose "------------------------"
}
}

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