Category Archives: Windows Server

Converting group policy registry preferences to PowerShell scripts

Most of the clients at my firm are moving to cloud only solutions in which we have less management options available. We can use Intune for Administrative Templates, or as we do use our RMM system as the management platform. To make sure we can use our RMM system we have several scripts that deploy registry keys in the same way as the GPO does. If you want to find what keys a GPO sets you can use this website.

Now the issue with this is that you do not directly have the option to deploy Group Policy Preferences instead of Group Policy Administrative Templates. A lot of our clients have these for applications that do not support ADMX files. To convert these I’ve created the following script.

The conversion script

 function convert-gpo($GPOFilePath) {
    $XMLFile = get-content $GPOFilePath
    $RegKeys = ($xmlfile | Select-Xml -Xpath "//Registry").Node.properties
    foreach ($regkey in $RegKeys) {
        #Converting Hkey to actual path
        switch ($regkey.hive) {
            "HKEY_CURRENT_USER" { $regkey.hive = "HKCU:" ; break }
            "HKEY_LOCAL_MACHINE" { $regkey.hive = "HKLM:" ; break }
        }
        $RegPath = Join-Path $regkey.hive $regkey.key
        write-host "####### $($RegPath) with $($regkey.name) #######"
        write-host "New-ItemProperty -Path $($RegPath) -name $($regkey.name) -Value $($regkey.value) -Force"
        write-host "####### NEXT KEY #######"
    }
    
}

Using this script you can run the Convert-GPO function with the file path of the export of your GPO preference file as the only parameter.

The script will then write a new script using write-host. You can copy and paste this to your RMM system, or save it as a file by running "Convert-GPO $Filename | out-file NewScript.ps1" and that’s it! as always, Happy PowerShelling.

Monitoring with PowerShell: Monitoring Office365 admin password changes

So when I was at Dattocon I was approached by an MSP that was using his RMM system to alert on changes of the local admin password, as he wanted to be updated every time a local admin got a new password. He did this by using an older script of mine below.

Monitoring Local Admin Password changes

$LastDay = (Get-Date).addhours(-24)
$AdminGroup = Get-LocalGroupMember -SID "S-1-5-32-544"
foreach($Admin in $AdminGroup){
$ChangedAdmins = get-localuser -sid $admin.sid | Where-Object {$_.PasswordLastSet -gt $LastDay}
}

But he came to me telling me that recently he had a need to start using this to alert on that a password needed to be updated in his documentation system to complete a process, but he was missing this for Office365 environments. I figured I would give him a hand and made the following script

Monitoring Office365 Global Admin Password changes – All tenants

$LastDay = (Get-Date).addhours(-24)
$credential = Get-Credential
Connect-MsolService -Credential $credential
$customers = Get-msolpartnercontract -All
$ChangedUsers = @()
foreach($customer in $customers){
write-host "getting users for $($Customer.Name)" -ForegroundColorGreen
$adminemails = Get-MsolRoleMember -TenantId $customer.tenantid -RoleObjectId(Get-MsolRole-RoleName"CompanyAdministrator").ObjectId
$Users = $adminemails | get-msoluser-TenantId$customer.TenantId
foreach($User in $Users){
if($User.LastPasswordChangeTimestamp -gt $LastDay){$ChangedUsers += "$($User.UserPrincipalName)has changed his password in the last 24 hours.Please update documentation to reflect.`n"}
}
}

 

Monitoring Office365 Global Admin Password Changes – Single tenant

$TenantName = "YourTenantName.onmicrosoft.com"
$LastDay = (Get-Date).addhours(-24)
$credential = Get-Credential
Connect-MsolService -Credential $credential
$Customer=Get-msolpartnercontract -All | Where-Object{$_.DefaultDomainName -eq $TenantName}
$ChangedUsers=@()
write-host"getting users for $($Customer.Name)" -ForegroundColorGreen
$adminemails = Get-MsolRoleMember -TenantId$customer.tenantid -RoleObjectId (Get-MsolRole -RoleName "CompanyAdministrator").ObjectId
$Users= $adminemails | get-msoluser-TenantId $customer.TenantId
foreach($User in $Users){
if($User.LastPasswordChangeTimestamp -gt $LastDay){$ChangedUsers +="$($User.UserPrincipalName) has changed his password in the last 24 hours.Please update documentation to reflect.`n"}
}

 

This script checks if a password has been changed in the last day, and if so alerts on it, notifying you that a global admin password has been updated and needs to be changed in the documentation. You can also use this as a warning system if you do not have anyone that should be changing these passwords.

Anyway, hope it helps, and as always. Happy PowerShelling!

Dattocon!

Hi all,

I’ll be presenting at Dattocon next week, so I will not be able to release the new blogs about monitoring with PowerShell. If you’re coming to Dattocon feel free to join my session. you can find information about the session here. The description is a little bit off as I will be talking mostly about PowerShell and automation at MSPs.

I’ll upload all resources to this blog after, including some documentation, examples to use during scripting, and the slide deck & recording.

The normal blogging schedule will resume directly after the PowerShell for MSP’s webinar. To get tickets for that, click here.

Function: Write-DRRMAlert

New-DattoRMMAlert was shared to me by Stan Lee at Datto, Stan also loves the DRY principle of coding and as such I’m also sharing it with you. You can only alert single line items, and not arrays or multiline contents as Datto does not support this.

function write-DRRMAlert ($message) {
    write-host '<-Start Result->'
    write-host "Alert=$message"
    write-host '<-End Result->'
    }

Function: Write-DRMMDiag

Similar to the top one, but generated by myself is the diagnostics printing module. We can feed this anything from objects, to arrays, to single string items 🙂

function&nbsp;write-DRMMDiag&nbsp;($messages)&nbsp;{
write-host&nbsp; '&lt;-Start&nbsp;Diagnostic-&gt;'
foreach($Message&nbsp;in&nbsp;$Messages){&nbsp;$Message}
write-host&nbsp;'&lt;-End&nbsp;&nbsp;Diagnostic-&gt;'
}

Documenting with PowerShell: Bulk edit configurations in IT-Glue

I know last week I said I’d take a break from the monitoring blogs, but a MSP recently requested if I knew a way to mass-edit specific configuration items in IT-Glue. In his case, he was going to change the network configuration of devices and wanted a quicker way than to just click on 20 devices. It would be getting annoying fast to do that via the interface.

To make these edits easier for him, I’ve decided to quickly script the following for him:

    #####################################################################
    $APIKEy =  "APIKEYHERE"
    $APIEndpoint = "https://api.eu.itglue.com"
    $orgID = "ORGIDHERE"
    $NewGateway = "192.1.1.254"
    #####################################################################
    If(Get-Module -ListAvailable -Name "ITGlueAPI") {Import-module ITGlueAPI} Else { install-module ITGlueAPI -Force; import-module ITGlueAPI}
    #Settings IT-Glue logon information
    Add-ITGlueBaseURI -base_uri $APIEndpoint
    Add-ITGlueAPIKey $APIKEy
    $ConfigList = (Get-ITGlueConfigurations -page_size 1000 -organization_id $OrgID).data.attributes | Out-GridView -PassThru
    foreach($Config in $ConfigList){
    $ConfigID = ($config.'resource-url' -split "/")[-1]
    $UpdatedConfig = 
    @{
        type = 'Configurations'
        attributes = @{
                    "default-gateway" = $NewGateway
        }
    }
    Set-ITGlueConfigurations -id $ConfigID -data $UpdatedConfig
    }

This grabs all configurations for the specific organisation ID you’ve filled in, it then gives you a grid with all the current configurations. Using this grid you can select the configurations you’d want to make a change and apply the new gateway. Its very easy to modify other fields in bulk too, for this, check the API documentation here.

Anyway, I hope it helps some people struggling with bulk edits, and as always, happy PowerShelling!

Monitoring with PowerShell: Monitoring Cipher suites (And get a SSLLabs A rank)

I always like getting the maximum achievable rank on websites such as SSLLabs, or the Microsoft Secure Score, because I know I’ve done all that a manufacturer says I need to do to protect their product. The SSL cipher suites are one of these things.

You can run the following script on both Windows Servers that are running IIS to achieve a SSLLabs A rank, but also you can run this script on client machines to increase the security so they will not use older ciphers when requested.

The monitoring script

Monitoring the cipher suites is fairly straightforward. First we’ll check if TLS1.0 and TLS1.1 are disabled and if TLS1.2 is enabled, After that, we check if old know “bad” ciphers are no longer used.

$SChannel = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols" 
$TLS1ServerEnabled = Get-ItemProperty -Path "$($SChannel)\TLS 1.0\Server" -Name Enabled -ErrorAction SilentlyContinue
$TLS11ServerEnabled = Get-ItemProperty -Path "$($SChannel)\TLS 1.1\Server" -Name Enabled -ErrorAction SilentlyContinue
$TLS11ClientEnabled = get-ItemProperty -Path "$($SChannel)\TLS 1.1\Client" -Name Enabled -ErrorAction SilentlyContinue
$TLS12ServerEnabled = get-ItemProperty -Path "$($SChannel)\TLS 1.2\Server" -Name Enabled -ErrorAction SilentlyContinue
$TLS12ClientEnabled = get-ItemProperty -Path "$($SChannel)\TLS 1.2\Client" -Name Enabled -ErrorAction SilentlyContinue

If(!$TLS1ServerEnabled -or $TLS1ServerEnabled -eq 1) { $TLS1ServerEnabled = "TLS Server 1.0 could be enabled. Please investigate"  } else { $TLS1ServerEnabled = "Healthy" }
If(!$TLS11ServerEnabled -or $TLS11ServerEnabled -eq 1) { $TLS11ServerEnabled = "TLS Server 1.1 could be enabled. Please investigate"  }  else { $TLS11ServerEnabled = "Healthy" }
If(!$TLS11ClientEnabled -or $TLS11ClientEnabled -eq 1) { $TLS11ClientEnabled = "TLS Client 1.1 could be enabled.. Please investigate"  }  else { $TLS11ClientEnabled = "Healthy" }
If(!$TLS12ServerEnabled -or $TLS12ServerEnabled -eq 0) { $TLS12ServerEnabled = "TLS Server 1.2 could be disabled. Please investigate"  }  else { $TLS12ServerEnabled = "Healthy" }
If(!$TLS12ClientEnabled -or $TLS12ClientEnabled -eq 0) { $TLS12ClientEnabled = "TLS Client 1.2 could be disabled Please investigate"  }  else { $TLS12ClientEnabled = "Healthy" }

$OldCipherSuites =
@(
    "TLS_DHE_RSA_WITH_AES_256_CBC_SHA"
    "TLS_DHE_RSA_WITH_AES_128_CBC_SHA"
    "TLS_RSA_WITH_AES_256_GCM_SHA384"
    "TLS_RSA_WITH_AES_128_GCM_SHA256"
    "TLS_RSA_WITH_AES_256_CBC_SHA256"
    "TLS_RSA_WITH_AES_128_CBC_SHA256"
    "TLS_RSA_WITH_AES_256_CBC_SHA"
    "TLS_RSA_WITH_AES_128_CBC_SHA"
    "TLS_RSA_WITH_3DES_EDE_CBC_SHA"
    "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256"
    "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256"
    "TLS_DHE_DSS_WITH_AES_256_CBC_SHA"
    "TLS_DHE_DSS_WITH_AES_128_CBC_SHA"
    "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA"
    "TLS_RSA_WITH_RC4_128_SHA"
    "TLS_RSA_WITH_RC4_128_MD5"
    "TLS_RSA_WITH_NULL_SHA256"
    "TLS_RSA_WITH_NULL_SHA"
    "TLS_PSK_WITH_AES_256_GCM_SHA384"
    "TLS_PSK_WITH_AES_128_GCM_SHA256"
    "TLS_PSK_WITH_AES_256_CBC_SHA384"
    "TLS_PSK_WITH_AES_128_CBC_SHA256"
    "TLS_PSK_WITH_NULL_SHA384"
    "TLS_PSK_WITH_NULL_SHA256"
)
$SuitesEnabled = @()
foreach($Suite in $OldCipherSuites){
$SuitesEnabled += get-TlsCipherSuite -name $Suite
}
if(!$SuitesEnabled){
$SuitesEnabled = "Healthy. No old cipher suites found"
} else {
$SuitesEnabled = "Possible old cipher suites found"
} 

After you run this script, you can alert on the contents of $SuitesEnabled to see if old cipher suites are enabled. You also should alert on the content of the following five variables to make sure that you have them all in a “Healthy” state

$TLS1ServerEnabled
$TLS11ServerEnabled
$TLS11ClientEnabled 
$TLS12ServerEnabled
$TLS12ClientEnabled

The remediation script

the remediation is actually very similar to the script above, but we change to create the registry keys this time, and to disable the cipher suites using disable-Tlsciphersuite.

$SChannel = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols"
New-Item "$($SChannel)\TLS 1.2\Server" -Force
New-Item "$($SChannel)\TLS 1.2\Client" -Force
New-Item $SChannel -Name "TLS 1.0"
New-Item "$($SChannel)\TLS 1.0" -Name Server
New-Item "$($SChannel)\TLS 1.1\Server" –force
New-Item "$($SChannel)\TLS 1.1\Client" –force
New-ItemProperty -Path "$($SChannel)\TLS 1.0\Server" -Name Enabled -Value 0 -PropertyType DWORD
New-ItemProperty -Path "$($SChannel)\TLS 1.1\Server" -Name Enabled -Value 0 -PropertyType DWORD
New-ItemProperty -Path "$($SChannel)\TLS 1.1\Server" -Name DisabledByDefault -Value 0 -PropertyType DWORD
New-ItemProperty -Path "$($SChannel)\TLS 1.1\Client" -Name Enabled -Value 0 -PropertyType DWORD
New-ItemProperty -Path "$($SChannel)\TLS 1.1\Client" -Name DisabledByDefault -Value 0 -PropertyType DWORD
New-ItemProperty -Path "$($SChannel)\TLS 1.2\Server" -Name Enabled -Value 1 -PropertyType DWORD
New-ItemProperty -Path "$($SChannel)\TLS 1.2\Server" -Name DisabledByDefault -Value 0 -PropertyType DWORD
New-ItemProperty -Path "$($SChannel)\TLS 1.2\Client" -Name Enabled -Value 1 -PropertyType DWORD
New-ItemProperty -Path "$($SChannel)\TLS 1.2\Client" -Name DisabledByDefault -Value 0 -PropertyType DWORD
$OldCipherSuites =
@(
    "TLS_DHE_RSA_WITH_AES_256_CBC_SHA"
    "TLS_DHE_RSA_WITH_AES_128_CBC_SHA"
    "TLS_RSA_WITH_AES_256_GCM_SHA384"
    "TLS_RSA_WITH_AES_128_GCM_SHA256"
    "TLS_RSA_WITH_AES_256_CBC_SHA256"
    "TLS_RSA_WITH_AES_128_CBC_SHA256"
    "TLS_RSA_WITH_AES_256_CBC_SHA"
    "TLS_RSA_WITH_AES_128_CBC_SHA"
    "TLS_RSA_WITH_3DES_EDE_CBC_SHA"
    "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256"
    "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256"
    "TLS_DHE_DSS_WITH_AES_256_CBC_SHA"
    "TLS_DHE_DSS_WITH_AES_128_CBC_SHA"
    "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA"
    "TLS_RSA_WITH_RC4_128_SHA"
    "TLS_RSA_WITH_RC4_128_MD5"
    "TLS_RSA_WITH_NULL_SHA256"
    "TLS_RSA_WITH_NULL_SHA"
    "TLS_PSK_WITH_AES_256_GCM_SHA384"
    "TLS_PSK_WITH_AES_128_GCM_SHA256"
    "TLS_PSK_WITH_AES_256_CBC_SHA384"
    "TLS_PSK_WITH_AES_128_CBC_SHA256"
    "TLS_PSK_WITH_NULL_SHA384"
    "TLS_PSK_WITH_NULL_SHA256"
)
foreach($Suite in $OldCipherSuites){
disable-TlsCipherSuite -name $Suite -ErrorAction SilentlyContinue
} 

And that’s it! I hope you’ve enjoyed and as always, Happy PowerShelling

Documenting with PowerShell Chapter 6: Documenting Active Directory groups

This will be the last post in the documenting with PowerShell series for a short while. I’ve enjoyed the series thoroughly but there are so many choices to blog about and I want to take a short break to be able to prepare the next series with all the requests I’ve been getting.

This time we will get al the current active directory groups, list all users in these groups, and even attach the contact as a tagged resource in IT-Glue. This way, you can look up a specific contact and find that exactly in which groups they’ve been added. It’s also pretty cool to combine this script with the previous blog found here.

The script

     #####################################################################
    $APIKEy =  "APIKEYHERE"
    $APIEndpoint = "https://api.eu.itglue.com"
    $orgID = "ORGIDHERE"
    #Tag related devices. this will try to find the devices based on the MAC, Connected to this network, and tag them as related devices.
    $FlexAssetName = "ITGLue AutoDoc - Active Directory Groups v2"
    $Description = "Lists all groups and users in them."
    #####################################################################
    If(Get-Module -ListAvailable -Name "ITGlueAPI") {Import-module ITGlueAPI} Else { install-module ITGlueAPI -Force; import-module ITGlueAPI}
    #Settings IT-Glue logon information
    Add-ITGlueBaseURI -base_uri $APIEndpoint
    Add-ITGlueAPIKey $APIKEy
    #Collect Data
    $AllGroups = get-adgroup -filter *
    foreach($Group in $AllGroups){
$Contacts = @()
    $Members = get-adgroupmember $Group
    $MembersTable = $members | Select-Object Name, distinguishedName | ConvertTo-Html -Fragment | Out-String
    foreach($Member in $Members){
 
    $email = (get-aduser $member -Properties EmailAddress).EmailAddress
    #Tagging devices
            if($email){
            Write-Host "Finding all related contacts - Based on email: $email"
            $Contacts += (Get-ITGlueContacts -page_size "1000" -filter_primary_email $email).data
            }
    }
    $FlexAssetBody = 
    @{
        type = 'flexible-assets'
        attributes = @{
                name = $FlexAssetName
                traits = @{
                    "group-name" = $($group.name)
                    "members" = $MembersTable
                    "guid" = $($group.objectguid.guid)
                    "tagged-users" = $Contacts.id
                }
        }
    }
    #Checking if the FlexibleAsset exists. If not, create a new one.
    $FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
    if(!$FilterID){ 
        $NewFlexAssetData = 
        @{
            type = 'flexible-asset-types'
            attributes = @{
                    name = $FlexAssetName
                    icon = 'sitemap'
                    description = $description
            }
            relationships = @{
                "flexible-asset-fields" = @{
                    data = @(
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order           = 1
                                name            = "Group Name"
                                kind            = "Text"
                                required        = $true
                                "show-in-list"  = $true
                                "use-for-title" = $true
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 2
                                name           = "Members"
                                kind           = "Textbox"
                                required       = $false
                                "show-in-list" = $true
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 3
                                name           = "GUID"
                                kind           = "Text"
                                required       = $false
                                "show-in-list" = $false
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 4
                                name           = "Tagged Users"
                                kind           = "Tag"
                                "tag-type"     = "Contacts"
                                required       = $false
                                "show-in-list" = $false
                            }
                       
                        }
                    )
                    }
                }
                  
           }
    New-ITGlueFlexibleAssetTypes -Data $NewFlexAssetData 
    $FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
    } 
    #Upload data to IT-Glue. We try to match the Server name to current computer name.
    $ExistingFlexAsset = (Get-ITGlueFlexibleAssets -filter_flexible_asset_type_id $Filterid.id -filter_organization_id $orgID).data | Where-Object {$_.attributes.traits.'group-name' -eq $($group.name)}
    #If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
    if(!$ExistingFlexAsset){
    $FlexAssetBody.attributes.add('organization-id', $orgID)
    $FlexAssetBody.attributes.add('flexible-asset-type-id', $FilterID.id)
    Write-Host "Creating new flexible asset"
    New-ITGlueFlexibleAssets -data $FlexAssetBody
    } else {
    Write-Host "Updating Flexible Asset"
    $ExistingFlexAsset = $ExistingFlexAsset[-1]
    Set-ITGlueFlexibleAssets -id $ExistingFlexAsset.id  -data $FlexAssetBody}
    } 

And that’s it. This will help you document all your security and distribution groups. You’ll even see them in the contact sidebar, so you have a quick overview what user is in what groups.

And that’s it! as always, Happy PowerShelling.

Documenting with PowerShell Chapter 5: File share permissions

A lot of our clients struggle with permission management. Some of our clients practically want a daily permissions report to see if anything has changed. To make sure that our client can be sure we manage their permissions correctly we always upload the permission sets to our IT-glue documentation system. This way the client can see whenever permissions have changed and look at how the state of the permissions was last week, last month, or even last year.

Today I’ll be showing you how to upload permissions to IT-Glue using the IT-Glue API and the NTFSSecurity module. We’;ll also have a generic non-ITGlue version you can use with whatever documentation system you use.

IT-Glue version

We’ll be using the same base script as with the other chapters. Remember to set at the variables to your environment. The script will gather the permissions of the root folder specified, and all unique permissions of files and folders under that. It will skip over inherited permissions from the root.

The recursive depth is currently set to only look 2 folders deep. This is because our permission system does not allow unique permissions deeper than this. If your process does you should change the variable $RecursiveDepth.

Remember; it will make the flexible asset for you, if it does not yet exist.

    #####################################################################
    $APIKEy =  "APIKEYHERE"
    $APIEndpoint = "https://api.eu.itglue.com"
    $orgID = "ORGIDHERE"
    $FlexAssetName = "ITGLue AutoDoc - File Share v2"
    $Description = "a list of unique file share permissions"
    $RecursiveDepth = 2
    #####################################################################
    If(Get-Module -ListAvailable -Name "ITGlueAPI") {Import-module ITGlueAPI} Else { install-module ITGlueAPI -Force; import-module ITGlueAPI}
    If(Get-Module -ListAvailable -Name "NTFSSecurity") {Import-module "NTFSSecurity"} Else { install-module "NTFSSecurity" -Force; import-module "NTFSSecurity"}
    #Settings IT-Glue logon information
    Add-ITGlueBaseURI -base_uri $APIEndpoint
    Add-ITGlueAPIKey $APIKEy
    #Collect Data
    $AllsmbShares = get-smbshare | Where-Object {(@('Remote Admin','Default share','Remote IPC') -notcontains $_.Description)}
    foreach($SMBShare in $AllSMBShares){
    $Permissions = get-item $SMBShare.path | get-ntfsaccess
    $Permissions += get-childitem -Depth $RecursiveDepth -Recurse $SMBShare.path | get-ntfsaccess
    $FullAccess = $permissions | where-object {$_.'AccessRights' -eq "FullControl" -AND $_.IsInherited -eq $false -AND $_.'AccessControlType' -ne "Deny"}| Select-Object FullName,Account,AccessRights,AccessControlType  | ConvertTo-Html -Fragment | Out-String
    $Modify = $permissions | where-object {$_.'AccessRights' -Match "Modify" -AND $_.IsInherited -eq $false -and $_.'AccessControlType' -ne "Deny"}| Select-Object FullName,Account,AccessRights,AccessControlType  | ConvertTo-Html -Fragment | Out-String
    $ReadOnly = $permissions | where-object {$_.'AccessRights' -Match "Read" -AND $_.IsInherited -eq $false -and $_.'AccessControlType' -ne "Deny"}| Select-Object FullName,Account,AccessRights,AccessControlType  | ConvertTo-Html -Fragment | Out-String
    $Deny =   $permissions | where-object {$_.'AccessControlType' -eq "Deny" -AND $_.IsInherited -eq $false} | Select-Object FullName,Account,AccessRights,AccessControlType | ConvertTo-Html -Fragment | Out-String

if($FullAccess.Length /1kb -gt 64) { $FullAccess = "The table is too long to display. Please see included CSV file."}
if($ReadOnly.Length /1kb -gt 64) { $ReadOnly = "The table is too long to display. Please see included CSV file."}
if($Modify.Length /1kb -gt 64) { $Modify = "The table is too long to display. Please see included CSV file."}
if($Deny.Length /1kb -gt 64) { $Deny = "The table is too long to display. Please see included CSV file."}
$PermCSV = ($Permissions | ConvertTo-Csv -NoTypeInformation -Delimiter ",") -join [Environment]::NewLine
$Bytes = [System.Text.Encoding]::UTF8.GetBytes($PermCSV)
$Base64CSV =[Convert]::ToBase64String($Bytes)    
    #Tagging devices
        $DeviceAsset = @()
        If($TagRelatedDevices -eq $true){
            Write-Host "Finding all related resources - Based on computername: $ENV:COMPUTERNAME"
            foreach($hostfound in $networkscan | Where-Object { $_.Ping -ne $false}){
            $DeviceAsset += (Get-ITGlueConfigurations -page_size "1000" -filter_name $ENV:COMPUTERNAME -organization_id $orgID).data }
            }     
    $FlexAssetBody = 
    @{
        type = 'flexible-assets'
        attributes = @{
                name = $FlexAssetName
                traits = @{
                    "share-name" = $($smbshare.name)
                    "share-path" = $($smbshare.path)
                    "full-control-permissions" = $FullAccess
                    "read-permissions" = $ReadOnly
                    "modify-permissions" = $Modify
                    "deny-permissions" = $Deny
                    "tagged-devices" = $DeviceAsset.ID
                    "csv-file" = @{
                        "content" = $Base64CSV
                        "file_name" = "Permissions.csv"
                    }
                }
        }
    }
    #Checking if the FlexibleAsset exists. If not, create a new one.
    $FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
    if(!$FilterID){ 
        $NewFlexAssetData = 
        @{
            type = 'flexible-asset-types'
            attributes = @{
                    name = $FlexAssetName
                    icon = 'sitemap'
                    description = $description
            }
            relationships = @{
                "flexible-asset-fields" = @{
                    data = @(
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order           = 1
                                name            = "Share Name"
                                kind            = "Text"
                                required        = $true
                                "show-in-list"  = $true
                                "use-for-title" = $true
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 2
                                name           = "Share Path"
                                kind           = "Text"
                                required       = $false
                                "show-in-list" = $true
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 3
                                name           = "Full Control Permissions"
                                kind           = "Textbox"
                                required       = $false
                                "show-in-list" = $false
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 4
                                name           = "Modify Permissions"
                                kind           = "Textbox"
                                required       = $false
                                "show-in-list" = $false
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 5
                                name           = "Read permissions"
                                kind           = "Textbox"
                                required       = $false
                                "show-in-list" = $false
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 6
                                name           = "Deny permissions"
                                kind           = "Textbox"
                                required       = $false
                                "show-in-list" = $false
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 7
                                name           = "CSV File"
                                kind           = "Upload"
                                required       = $false
                                "show-in-list" = $false
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 8
                                name           = "Tagged Devices"
                                kind           = "Tag"
                                "tag-type"     = "Configurations"
                                required       = $false
                                "show-in-list" = $false
                            }
                        }
                    )
                    }
                }
                  
           }
    New-ITGlueFlexibleAssetTypes -Data $NewFlexAssetData 
    $FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
    } 
    #Upload data to IT-Glue. We try to match the Server name to current computer name.
    $ExistingFlexAsset = (Get-ITGlueFlexibleAssets -filter_flexible_asset_type_id $Filterid.id -filter_organization_id $orgID).data | Where-Object {$_.attributes.name -eq $($SMBShare.name)}
    #If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
    if(!$ExistingFlexAsset){
    $FlexAssetBody.attributes.add('organization-id', $orgID)
    $FlexAssetBody.attributes.add('flexible-asset-type-id', $FilterID.id)
    Write-Host "Creating new flexible asset"
    New-ITGlueFlexibleAssets -data $FlexAssetBody
    } else {
    Write-Host "Updating Flexible Asset"
    $ExistingFlexAsset = $ExistingFlexAsset[-1]
    Set-ITGlueFlexibleAssets -id $ExistingFlexAsset.id  -data $FlexAssetBody}
    }

The generated file is also uploaded as an attachment to the Flexible asset, so you can always check the CSV and manipulate or re-import it as your see fit. If the HTML table is larger than 64KB it also will not display the HTML table, as the maximum HTML table size for ITGlue is 64KB.

General version

$RecursiveDepth = 2
 

$head = @"
<script>
function myFunction() {
    const filter = document.querySelector('#myInput').value.toUpperCase();
    const trs = document.querySelectorAll('table tr:not(.header)');
    trs.forEach(tr => tr.style.display = [...tr.children].find(td => td.innerHTML.toUpperCase().includes(filter)) ? '' : 'none');
  }</script>
<Title>Audit Log Report</Title>
<style>
body { background-color:#E5E4E2;
      font-family:Monospace;
      font-size:10pt; }
td, th { border:0px solid black; 
        border-collapse:collapse;
        white-space:pre; }
th { color:white;
    background-color:black; }
table, tr, td, th {
     padding: 2px; 
     margin: 0px;
     white-space:pre; }
tr:nth-child(odd) {background-color: lightgray}
table { width:95%;margin-left:5px; margin-bottom:20px; }
h2 {
font-family:Tahoma;
color:#6D7B8D;
}
.footer 
{ color:green; 
 margin-left:10px; 
 font-family:Tahoma;
 font-size:8pt;
 font-style:italic;
}
#myInput {
  background-image: url('https://www.w3schools.com/css/searchicon.png'); /* Add a search icon to input */
  background-position: 10px 12px; /* Position the search icon */
  background-repeat: no-repeat; /* Do not repeat the icon image */
  width: 50%; /* Full-width */
  font-size: 16px; /* Increase font-size */
  padding: 12px 20px 12px 40px; /* Add some padding */
  border: 1px solid #ddd; /* Add a grey border */
  margin-bottom: 12px; /* Add some space below the input */
}
</style>
"@

If(Get-Module -ListAvailable -Name "NTFSSecurity") {Import-module "NTFSSecurity"} Else { install-module "NTFSSecurity" -Force; import-module "NTFSSecurity"}
     
$AllsmbShares = get-smbshare | Where-Object {(@('Remote Admin','Default share','Remote IPC') -notcontains $_.Description)}
foreach($SMBShare in $AllSMBShares){
$Permissions = get-item $SMBShare.path | get-ntfsaccess
$Permissions += get-childitem -Depth $RecursiveDepth -Recurse $SMBShare.path | get-ntfsaccess
$FullAccess = $permissions | where-object {$_.'AccessRights' -eq "FullControl" -AND $_.IsInherited -eq $false -AND $_.'AccessControlType' -ne "Deny"}| Select-Object FullName,Account,AccessRights,AccessControlType  | ConvertTo-Html -PreContent "<h1>Full Access</h1>" -Fragment | Out-String
$Modify = $permissions | where-object {$_.'AccessRights' -Match "Modify" -AND $_.IsInherited -eq $false -and $_.'AccessControlType' -ne "Deny"}| Select-Object FullName,Account,AccessRights,AccessControlType  | ConvertTo-Html "<h1>Modify</h1>"  -Fragment | Out-String
$ReadOnly = $permissions | where-object {$_.'AccessRights' -Match "Read" -AND $_.IsInherited -eq $false -and $_.'AccessControlType' -ne "Deny"}| Select-Object FullName,Account,AccessRights,AccessControlType  | ConvertTo-Html "<h1>Read Only</h1>" -Fragment | Out-String
$Deny =   $permissions | where-object {$_.'AccessControlType' -eq "Deny" -AND $_.IsInherited -eq $false} | Select-Object FullName,Account,AccessRights,AccessControlType | ConvertTo-Html -Fragment "<h1>Deny</h1>" | Out-String
$PermCSV = $Permissions | ConvertTo-Csv -Delimiter "," | out-file "C:\Export\ExportOfPermissions.csv" -append
$head,$FullAccess,$Modify,$ReadOnly,$Deny | Out-File "C:\Export\$($SMBShare.name).html"
}

And that’s it! As always, Happy PowerShelling!

Documenting with PowerShell Chapter 4: Network documentation for IT-Glue.

In the last couple of blogs we spoke about how to handle passwords, passwords objects and tagging, and how to start documenting your servers. Today, We’re starting on the network side of documentation. Within IT-Glue it’s important to follow the IT-Glue standards so your documentation works with the relational database it was designed on.

There are a lot of tools that make automatic documation possible but pass around the fact that IT-Glue works best if you develop your documentation based on their best practices. This is why the documentation script we’ll make for networking has those in place: The script tags all available resources it can find, that are related to the network.

We currently tag all devices based on their primary IP in IT-Glue. You will need these filled in correctly to make sure the documentation script picks these up.

The Script

The script creates a flexible asset for you, with the required fields if the asset is not yet available. The flexible Asset will have 6 fields: the subnet, the gateway, the DNS servers, the DHCP servers, and the tagged devices. It also has a single textbox where a copy of the network scan will be added.

The script uses the PSnmap module for the network scan, it will automatically download this from the PSGallery. More info about PSNmap can be found here.

    #####################################################################
    $APIKEy =  "ITGLUEAPIKEYHERE"
    $APIEndpoint = "https://api.eu.itglue.com"
    $orgID = "ORGIDHERE"
    #Tag related devices. this will try to find the devices based on the MAC, Connected to this network, and tag them as related devices.
    $TagRelatedDevices = $true
    $FlexAssetName = "ITGLue AutoDoc - Network overview v2"
    $Description = "a network one-page document that shows the current configuration found."
    #####################################################################
    $ConnectedNetworks = Get-NetIPConfiguration -Detailed | Where-Object {$_.Netadapter.status -eq "up"}

    If(Get-Module -ListAvailable -Name "ITGlueAPI") {Import-module ITGlueAPI} Else { install-module ITGlueAPI -Force; import-module ITGlueAPI}
    If(Get-Module -ListAvailable -Name "PSnmap") {Import-module "PSnmap"} Else { install-module "PSnmap" -Force; import-module "PSnmap"}
        #Settings IT-Glue logon information
        Add-ITGlueBaseURI -base_uri $APIEndpoint
        Add-ITGlueAPIKey $APIKEy
    foreach($Network in $ConnectedNetworks){ 
    $DHCPServer = (Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object { $_.IPAddress -eq $network.IPv4Address}).DHCPServer
    $Subnet = "$($network.IPv4DefaultGateway.nexthop)/$($network.IPv4Address.PrefixLength)"
    $NetWorkScan = Invoke-PSnmap -ComputerName $subnet -Port 80,443,3389,21,22,25,587 -Dns -NoSummary 
    $HTMLFrag = $NetworkScan | Where-Object {$_.Ping -eq $true} | convertto-html -Fragment -PreContent "<h1> Network scan of $($subnet) <br/><table class=`"table table-bordered table-hover`" >" | out-string
    #Tagging devices
    $DeviceAsset = @()
    If($TagRelatedDevices -eq $true){
        Write-Host "Finding all related resources - Matching on IP at local side, Primary IP on IT-Glue side."
        foreach($hostfound in $networkscan | Where-Object { $_.Ping -ne $false}){
        $DeviceAsset +=  (Get-ITGlueConfigurations -page_size "1000" -organization_id $orgID).data | Where-Object {$_.Attributes."Primary-IP" -eq $($hostfound.ComputerName)}
        }
        }
    
    $FlexAssetBody = 
    @{
        type = 'flexible-assets'
        attributes = @{
                name = $FlexAssetName
                traits = @{
                    "subnet-network" = "$Subnet"
                    "subnet-gateway" = $network.IPv4DefaultGateway.nexthop
                    "subnet-dns-servers" = $network.dnsserver.serveraddresses
                    "subnet-dhcp-servers" = $DHCPServer
                    "scan-results" = $HTMLFrag
                    "tagged-devices" = $DeviceAsset.ID
                }
        }
    }
    

    #Checking if the FlexibleAsset exists. If not, create a new one.
    $FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
    if(!$FilterID){ 
        $NewFlexAssetData = 
        @{
            type = 'flexible-asset-types'
            attributes = @{
                    name = $FlexAssetName
                    icon = 'sitemap'
                    description = $description
            }
            relationships = @{
                "flexible-asset-fields" = @{
                    data = @(
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order           = 1
                                name            = "Subnet Network"
                                kind            = "Text"
                                required        = $true
                                "show-in-list"  = $true
                                "use-for-title" = $true
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 2
                                name           = "Subnet Gateway"
                                kind           = "Text"
                                required       = $false
                                "show-in-list" = $false
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 3
                                name           = "Subnet DNS Servers"
                                kind           = "Text"
                                required       = $false
                                "show-in-list" = $false
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 4
                                name           = "Subnet DHCP Servers"
                                kind           = "Text"
                                required       = $false
                                "show-in-list" = $false
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 5
                                name           = "Tagged Devices"
                                kind           = "Tag"
                                "tag-type"     = "Configurations"
                                required       = $false
                                "show-in-list" = $false
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 6
                                name           = "Scan Results"
                                kind           = "Textbox"
                                required       = $false
                                "show-in-list" = $false
                            }
                        }
                    )
                    }
                }
                  
           }
    New-ITGlueFlexibleAssetTypes -Data $NewFlexAssetData 
    $FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
    } 
    #Upload data to IT-Glue. We try to match the Server name to current computer name.
    $ExistingFlexAsset = (Get-ITGlueFlexibleAssets -filter_flexible_asset_type_id $Filterid.id -filter_organization_id $orgID).data | Where-Object {$_.attributes.name -eq $Subnet}
    #If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
    if(!$ExistingFlexAsset){
    $FlexAssetBody.attributes.add('organization-id', $orgID)
    $FlexAssetBody.attributes.add('flexible-asset-type-id', $FilterID.id)
    Write-Host "Creating new flexible asset"
    New-ITGlueFlexibleAssets -data $FlexAssetBody
    } else {
    Write-Host "Updating Flexible Asset"
    $ExistingFlexAsset = $ExistingFlexAsset[-1]
    Set-ITGlueFlexibleAssets -id $ExistingFlexAsset.id  -data $FlexAssetBody}
    }

And that’s it! this script will document parts of your network for you, and is easy to edit to anything you’d like it to be. and as always, happy PowerShelling!

Monitoring with PowerShell: Monitoring Security state

After the last couple of blogs I’ve been asked how I monitor the security state of Windows Servers, so I figured I would create a blog about monitoring some security advisement. Of course there is another disclaimer involved.

Disclaimer: Monitoring these security settings is only a small part of what your entire security monitoring suite should look like. There are a lot more settings and changes you’d need to monitor than just these, but these are items that can be used as a early warning system.

Now that we’ve got that out of the way we can start our monitoring script. We will dissect the script together and have the complete version at the bottom of the page.

The Script

First we will start on monitoring debuggers. This can be done both on both workstations and on servers. Debuggers are often used to secretly start a different process with elevated credentials, or you can have a executable start without the user ever clicking on it.

$debug = Get-Childitem -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options" -Recurse | Where-Object { $_.Property -eq "Debugger" } | Where-Object { $_.pschildname -ne "DeviceCensus.exe" }
if(!$debug) { 
    $DebuggerFound = "Healthy - No Debugers found"
} else {
foreach($key in $debug){
$DebuggerFound += "$($key.pschildname) is debugged `n"
}
}

Using this we find exactly which process has a debugger attached. DeviceCensus.exe always has a debugger attached so we can ignore this executable. Next we’ll be moving on to WDigest monitoring.

WDigest was a protocol that was introduced in the Windows XP time, the idea at that time was that this was to be used for web based authentication. Wdigest is enabled by default from server 2003 until Server 2012R2. The problem is that to have wdigest run correctly plain-text passwords got stored in LLASS. To resolve this Microsoft released an update to make sure you can disable wdigest on systems.

The problem is that all that is required to enable wdigest again is to change a registry key, We are going to monitor this key with two simple PowerShell commands. You want these items to be set to 0. If not, you should resolve by setting it to 0.

$WDigestNegotiate           = get-childitem -path "HKLM:\System\CurrentControlSet\Control\SecurityProviders\WDigest" | Where-Object {$_.Property -eq "Negotiate"}
$WDigestUseLogonCredential  =  get-childitem -path "HKLM:\System\CurrentControlSet\Control\SecurityProviders\WDigest" | Where-Object {$_.Property -eq "UseLogonCredential"}

Next up is Cached Credentials Account monitoring, again this can be used on both workstations and servers. Cached Credentials are used to logon when the domain controller is not available. For servers and workstations I would advise to lower this to 0. On laptops that is more difficult as users need to be able to work offline, currently we set it to 3 (2 for system logons, and 1 for the actual user account).

$CachedCredentialsAllowed   = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon").CachedLogonsCount

And as a last check we monitor the LM Compatibility Level. This declares what types of authentications can be used on the device. For more information on NTLM, LM Compatibility, and kerberos check this blog from Microsoft. We always completely go to the maximum security level of 5.

$NTLMCompatibilityLevel     = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa").lmcompatibilitylevel

And that’s it. Monitoring these items make your environment a little bit more secure, and protects you against most forms of Pass The Hash. The full script can be found below.

Full Script

$debug = Get-Childitem -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options" -Recurse | Where-Object { $_.Property -eq "Debugger" } | Where-Object { $_.pschildname -ne "DeviceCensus.exe" }
if(!$debug) { 
    $DebuggerFound = "Healthy - No Debugers found"
} else {
foreach($key in $debug){
$DebuggerFound += "$($key.pschildname) is debugged <br>`n"
}
}
$WDigestNegotiate           = get-childitem -path "HKLM:\System\CurrentControlSet\Control\SecurityProviders\WDigest" | Where-Object {$_.Property -eq "Negotiate"}
$WDigestUseLogonCredential  =  get-childitem -path "HKLM:\System\CurrentControlSet\Control\SecurityProviders\WDigest" | Where-Object {$_.Property -eq "UseLogonCredential"}
$CachedCredentialsAllowed   = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon").CachedLogonsCount
$NTLMCompatibilityLevel     = (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa").lmcompatibilitylevel

Monitoring with PowerShell: SMART status via CrystalDiskInfo

In a peer-group that I am a member of recently we’ve had a small discussion about monitoring the SMART status of hard drives. We all agreed that the issue with SMART monitoring is that often it is unreliable when using RMM systems. This is due to RMM systems using only the Windows SMART output which lacks some critical values you should monitor. SMART itself could be a pretty decent early warning system when using all values supplied.

To resolve this, I’ve created a set that uses CrystalDiskInfo. A tool made by CrystalMark which presents the values to you in a nice overview. We’ve used this in the past to troubleshoot or check disks for predictive failures manually, but figured we should also try the same automated. This piece of PowerShell makes SMART monitoring more agile and reliable, because we alert on more information than just the predicted failure values.

The script relies on Invoke-expression, and expand-archive, as such at least Windows 8.1 will be required.

The script

As always, the script is self-explanatory. Please upload the zip file to your own web server or location to where the latest version of CrystalDiskInfo is hosted. This also creates a folder in program program files directory and unzips itself there.

#Replace the Download URL to where you've uploaded the ZIP file yourself. We will only download this file once. 
$DownloadURL = "http://rwthaachen.dl.osdn.jp/crystaldiskinfo/71535/CrystalDiskInfo8_3_0.zip"
$DownloadLocation = "$($Env:ProgramFiles)\CrystalDiskInfo"
#Script: 
$TestDownloadLocation = Test-Path $DownloadLocation
if(!$TestDownloadLocation){
new-item $DownloadLocation -ItemType Directory -force
Invoke-WebRequest -Uri $DownloadURL -OutFile "$($DownloadLocation)\CrystalDiskInfo.zip"
Expand-Archive "$($DownloadLocation)\CrystalDiskInfo.zip" -DestinationPath $DownloadLocation -Force
}
#We start CrystalDiskInfo with the COPYEXIT parameter. This just collects the SMART information in DiskInfo.txt
Start-Process "$($Env:ProgramFiles)\CrystalDiskInfo\DiskInfo64.exe" -ArgumentList "/CopyExit" -wait
$DiskInfoRaw  = get-content "$($Env:ProgramFiles)\CrystalDiskInfo\DiskInfo.txt" | select-string "-- S.M.A.R.T. --------------------------------------------------------------" -Context 0,16
$diskinfo = $DiskInfoRaw -split "`n" | select -skip 2 | Out-String | convertfrom-csv -Delimiter " " -Header "NOTUSED1","NOTUSED2","ID","RawValue" | Select-Object ID,RawValue

[int64]$CriticalWarnings = "0x" + ($diskinfo | Where-Object { $_.ID -eq "01"}).rawvalue
[int64]$CompositeTemp = "0x" + ($diskinfo | Where-Object { $_.ID -eq "02"}).rawvalue -273.15
[int64]$AvailableSpare = "0x" +($diskinfo | Where-Object { $_.ID -eq "03"}).rawvalue
[int64]$ControllerBusyTime ="0x" + ($diskinfo | Where-Object { $_.ID -eq "0A"}).rawvalue
[int64]$PowerCycles ="0x" + ($diskinfo | Where-Object { $_.ID -eq "0B"}).rawvalue
[int64]$PowerOnHours = "0x" + ($diskinfo | Where-Object { $_.ID -eq "0C"}).rawvalue
[int64]$UnsafeShutdowns = "0x" +($diskinfo | Where-Object { $_.ID -eq "0D"}).rawvalue
[int64]$IntegrityErrors ="0x" + ($diskinfo | Where-Object { $_.ID -eq "0E"}).rawvalue
[int64]$InformationLogEntries ="0x" + ($diskinfo | Where-Object { $_.ID -eq "0F"}).rawvalue

The output variables will always contain data, this data can be used to threshold against in your RMM system. The thresholds I would use are:

  • $CriticalWarnings = 0
  • $CompositeTemp = 55 (this is 55 degrees celsius)
  • $AvailableSpare = 50 (This means there are 50 reallocation blocks available. This is extremely preventive so you might want to tune it to your personal preference)
  • $ControllerBusyTime = Not monitored, currently only log this for reporting purposes
  • $PowerCycles = Not monitored, currently only log this for reporting purposes
  • $PowerOnHours = 40000 (This is around 5 years of constant runtime.)
  • $UnsafeShutdowns = 365 (I like to know if users are not shutting down their computers normally. This could also point at other software related problems.)
  • $IntegrityErrors = 1 (This is what Windows normally reports on. We want to know as soon as these issues arise)
  • $InformationLogEntries = 1 (How many events have been generated related to disk SMART events)

I hope this helps MSPs that are having issues with SMART monitoring in their RMM systems, anyway – As always, Happy PowerShelling!

Monitoring with PowerShell: Monitoring log on of specific users.

Hi Guys, This’ll be the last blog before I go on holidays, So enjoy it and see you all in two weeks.

This time we’re going to talk about montoring the logon of specific users. We use named accounts for all our engineers and want to alert if another account that is unnamed has been logged onto in an interactive session. To do this, we’ll use the WMI instrumentation Win32_LoggedOnUser.

Just as an extra disclaimer: Please remember that in co-managed or environments that belong to others you won’t be able to always perform all best practices. This script will mostly be used to monitor those messy environments, and give you a little bit more sense of extra security. My personal advice would always be to disable accounts that are no longer allowed to login, use managed service accounts without interactive permissions for services, and delete accounts of ex-employees directly after the leave the company.

The script

Let’s get started on the script. First we’ll have to define which accounts we do not want to have in interactive sessions:

$ForbiddenList = @("Cyberdrain","cyber","migration","administrator","admin","service-QuickBooks*","svc-QB*","ExEmployee1")

So in this list, we name all accounts that are forbidden. You can add any user you would like to this list, We manage a lot of servers, and sometimes after an engineer leaves our company servers are still logged in with the user that we’ve disabled/deleted. With this script we also monitor those situations and log out the deleted user from all servers.

The next step is getting a list of the active users and comparing them:

$ActiveUsers = (Get-CimInstance Win32_LoggedOnUser).antecedent | Select-Object -Unique | Where-Object {$_.name -in $ForbiddenList}

So here we get all users that are currently logged on to the machine, and compare them to our forbidden list, our $activeUsers variable only gets filled if there is a match in the list.

if(!$ActiveUsers){$ActiveUsers = 'false'}

And this last line says that if $ActiveUsers is empty, so no users have been found that are logged in and in our list, it will say “false”. The complete script is just 3 lines and can be found below.

$ForbiddenList = @("Cyberdrain","cyber","migration","administrator","admin","service-QuickBooks*","svc-QB*","ExEmployee1")
$ActiveUsers = (Get-CimInstance Win32_LoggedOnUser).antecedent | Select-Object -Unique | Where-Object {$_.name -in $ForbiddenList}
if(!$ActiveUsers){$ActiveUsers = 'false'}

And that’s it. Some monitoring for situations you do not want to end up in. Remember to always follow security best practices first. Only use these scripts as an early warning system that someone, somewhere has made a mistake. 🙂 As always, Happy PowerShelling!

Documenting with PowerShell: Chapter 1 – Server overview page

In the previous blog I’ve showed how to upload data to IT-Glue by creating a new flexible asset with two fields: a name field on which we perform a match if a document exists, and a field where we placed some data. This time, we’re going to upload an entire HTML file we’ll create by gathering data from the server we run the script on. So, let’s get started

The scripts below can also be downloaded for your RMM system, find the AMP file here.

Data gathering

First, We’ll make a list of all the data that is imported for us to have documented. For me, that would be the following list of information:

  • Server Name
  • Server type (A physical or virtual machine)
  • How much RAM the machine currently has assigned
  • The NIC configuration
  • What applications are installed
  • What server roles are installed
  • What the physical disk layout is
  • What the RAID layout is

Let’s get started with each of the data collection parts. In the following script we check if the model contains “Virtual” or “VMWare”. two signs that show if a machine is a physical or virtual machine.

$ComputerSystemInfo = Get-CimInstance -ClassName Win32_ComputerSystem
if($ComputerSystemInfo.model -match "Virtual" -or $ComputerSystemInfo.model -match "VMware") { $MachineType = "Virtual"} Else { $MachineType = "Physical"}

To get the amount of RAM we run the following oneliner

$RAM = (systeminfo | Select-String 'Total Physical Memory:').ToString().Split(':')[1].Trim()

Applications and roles are also quite easy to get

$ApplicationsFrag = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | Convertto-html -Fragment | select -skip 1
$ApplicationsTable = "<br/><table class=`"table table-bordered table-hover`" >" + $ApplicationsFrag

$RolesFrag = Get-WindowsFeature | Where-Object {$_.Installed -eq $True} | Select-Object displayname,name  | convertto-html -Fragment | Select-Object -Skip 1
$RolesTable = "<br/><table class=`"table table-bordered table-hover`" >" + $RolesFrag

For the NIC configuration, we need to do a little more work. We want the NIC configuration to show both IPv4 and IPv6 configurations. We also want to look up all NIC configurations, if a server has multiple NICs.

$networkName = Get-CimInstance -ClassName Win32_NetworkAdapter | Where-Object {$_.PhysicalAdapter -eq "True"} | Sort Index
$networkIP = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object {$_.MACAddress -gt 0} | Sort Index
$networkSummary = New-Object -TypeName 'System.Collections.ArrayList'

foreach($nic in $networkName) {
    $nic_conf = $networkIP | Where-Object {$_.Index -eq $nic.Index}
 
    $networkDetails = New-Object PSObject -Property @{
        Index                = [int]$nic.Index;
        AdapterName         = [string]$nic.NetConnectionID;
        Manufacturer         = [string]$nic.Manufacturer;
        Description          = [string]$nic.Description;
        MACAddress           = [string]$nic.MACAddress;
        IPEnabled            = [bool]$nic_conf.IPEnabled;
        IPAddress            = [string]$nic_conf.IPAddress;
        IPSubnet             = [string]$nic_conf.IPSubnet;
        DefaultGateway       = [string]$nic_conf.DefaultIPGateway;
        DHCPEnabled          = [string]$nic_conf.DHCPEnabled;
        DHCPServer           = [string]$nic_conf.DHCPServer;
        DNSServerSearchOrder = [string]$nic_conf.DNSServerSearchOrder;
    }
    $networkSummary += $networkDetails
}
$NicRawConf = $networkSummary | select AdapterName,IPaddress,IPSubnet,DefaultGateway,DNSServerSearchOrder,MACAddress | Convertto-html -Fragment | select -Skip 1
$NicConf = "<br/><table class=`"table table-bordered table-hover`" >" + $NicRawConf

If a server is a physical machine, and a Dell server I also want to see the disk configuration. For this, you’ll need OpenManage installed.

if($machineType -eq "Physical" -and $ComputerSystemInfo.Manufacturer -matchf "Dell"){
$DiskLayoutRaw = omreport storage pdisk controller=0 -fmt cdv
$DiskLayoutSemi = $DiskLayoutRaw |  select-string -SimpleMatch "ID,Status," -context 0,($DiskLayoutRaw).Length | convertfrom-csv -Delimiter "," | select Name,Status,Capacity,State,"Bus Protocol","Product ID","Serial No.","Part Number",Media | convertto-html -Fragment
$DiskLayoutTable = "<br/><table class=`"table table-bordered table-hover`" >" + $DiskLayoutsemi

#Try to get RAID layout
$RAIDLayoutRaw = omreport storage vdisk controller=0 -fmt cdv
$RAIDLayoutSemi = $RAIDLayoutRaw |  select-string -SimpleMatch "ID,Status," -context 0,($RAIDLayoutRaw).Length | convertfrom-csv -Delimiter "," | select Name,Status,State,Layout,"Device Name","Read Policy","Write Policy",Media |  convertto-html -Fragment
$RAIDLayoutTable = "<br/><table class=`"table table-bordered table-hover`" >" + $RAIDLayoutsemi
}else {
    $RAIDLayoutTable = "Could not get physical disk info"
    $DiskLayoutTable = "Could not get physical disk info"
}

Putting all of that together will give us the following script, I’ve also added some headers to make the output HTML file look nice. You can use this as a stand-alone script.

#Server documentation script
$ComputerSystemInfo = Get-CimInstance -ClassName Win32_ComputerSystem
if($ComputerSystemInfo.model -match "Virtual" -or $ComputerSystemInfo.model -match "VMware") { $MachineType = "Virtual"} Else { $MachineType = "Physical"}
$networkName = Get-CimInstance -ClassName Win32_NetworkAdapter | Where-Object {$_.PhysicalAdapter -eq "True"} | Sort Index
$networkIP = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object {$_.MACAddress -gt 0} | Sort Index
$networkSummary = New-Object -TypeName 'System.Collections.ArrayList'

foreach($nic in $networkName) {
    $nic_conf = $networkIP | Where-Object {$_.Index -eq $nic.Index}
 
    $networkDetails = New-Object PSObject -Property @{
        Index                = [int]$nic.Index;
        AdapterName         = [string]$nic.NetConnectionID;
        Manufacturer         = [string]$nic.Manufacturer;
        Description          = [string]$nic.Description;
        MACAddress           = [string]$nic.MACAddress;
        IPEnabled            = [bool]$nic_conf.IPEnabled;
        IPAddress            = [string]$nic_conf.IPAddress;
        IPSubnet             = [string]$nic_conf.IPSubnet;
        DefaultGateway       = [string]$nic_conf.DefaultIPGateway;
        DHCPEnabled          = [string]$nic_conf.DHCPEnabled;
        DHCPServer           = [string]$nic_conf.DHCPServer;
        DNSServerSearchOrder = [string]$nic_conf.DNSServerSearchOrder;
    }
    $networkSummary += $networkDetails
}
$NicRawConf = $networkSummary | select AdapterName,IPaddress,IPSubnet,DefaultGateway,DNSServerSearchOrder,MACAddress | Convertto-html -Fragment | select -Skip 1
$NicConf = "<br/><table class=`"table table-bordered table-hover`" >" + $NicRawConf

$RAM = (systeminfo | Select-String 'Total Physical Memory:').ToString().Split(':')[1].Trim()

$ApplicationsFrag = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | Convertto-html -Fragment | select -skip 1
$ApplicationsTable = "<br/><table class=`"table table-bordered table-hover`" >" + $ApplicationsFrag

$RolesFrag = Get-WindowsFeature | Where-Object {$_.Installed -eq $True} | Select-Object displayname,name  | convertto-html -Fragment | Select-Object -Skip 1
$RolesTable = "<br/><table class=`"table table-bordered table-hover`" >" + $RolesFrag

if($machineType -eq "Physical" -and $ComputerSystemInfo.Manufacturer -match "Dell"){
$DiskLayoutRaw = omreport storage pdisk controller=0 -fmt cdv
$DiskLayoutSemi = $DiskLayoutRaw |  select-string -SimpleMatch "ID,Status," -context 0,($DiskLayoutRaw).Length | convertfrom-csv -Delimiter "," | select Name,Status,Capacity,State,"Bus Protocol","Product ID","Serial No.","Part Number",Media | convertto-html -Fragment
$DiskLayoutTable = "<br/><table class=`"table table-bordered table-hover`" >" + $DiskLayoutsemi

#Try to get RAID layout
$RAIDLayoutRaw = omreport storage vdisk controller=0 -fmt cdv
$RAIDLayoutSemi = $RAIDLayoutRaw |  select-string -SimpleMatch "ID,Status," -context 0,($RAIDLayoutRaw).Length | convertfrom-csv -Delimiter "," | select Name,Status,State,Layout,"Device Name","Read Policy","Write Policy",Media |  convertto-html -Fragment
$RAIDLayoutTable = "<br/><table class=`"table table-bordered table-hover`" >" + $RAIDLayoutsemi
}else {
    $RAIDLayoutTable = "Could not get physical disk info"
    $DiskLayoutTable = "Could not get physical disk info"
}
#Head for HTML
$head = @"
<Title>Server Log Report</Title>
<style>
body { background-color:#E5E4E2;
      font-family:Monospace;
      font-size:10pt; }
td, th { border:0px solid black; 
        border-collapse:collapse;
        white-space:pre; }
th { color:white;
    background-color:black; }
table, tr, td, th {
     padding: 2px; 
     margin: 0px;
     white-space:pre; }
tr:nth-child(odd) {background-color: lightgray}
table { width:95%;margin-left:5px; margin-bottom:20px; }
h2 {
font-family:Tahoma;
color:#6D7B8D;
}
.footer 
{ color:green; 
 margin-left:10px; 
 font-family:Tahoma;
 font-size:8pt;
 font-style:italic;
}
</style>
"@

$HTMLFile = @"
$head
<b>Servername</b>: $ENV:COMPUTERNAME <br>
<b>Server Type</b>: $machineType <br>
<b>Amount of RAM</b>: $RAM <br>
<br>
<h1>NIC Configuration</h1> <br>
$NicConf
<br>
<h1>Installed Applications</h1> <br>
$ApplicationsTable
<br>
<h1>Installed Roles</h1> <br>
$RolesTable
<br>
<h1>Physical Disk information</h1>
$DiskLayoutTable
<h1>RAID information</h1>
$RAIDLayoutTable
"@

$HTMLFile | out-file C:\Temp\ServerDoc.html

To upload this data to IT-Glue, we can use the exact same script as we’ve used last time, with two small edits; the title of the flexible asset, and the description.

#####################################################################
$APIKEy =  "YOUR API KEY GOES HERE"
$APIEndpoint = "https://api.eu.itglue.com"
$orgID = "THE ORGANISATIONID YOU WOULD LIKE TO UPDATE GOES HERE"
$FlexAssetName = "ITGLue AutoDoc - Server Overview"
$Description = "a server one-page document that shows the current configuration"
#####################################################################
#This is the object we'll be sending to IT-Glue. 
$ComputerSystemInfo = Get-CimInstance -ClassName Win32_ComputerSystem
if($ComputerSystemInfo.model -match "Virtual" -or $ComputerSystemInfo.model -match "VMware") { $MachineType = "Virtual"} Else { $MachineType = "Physical"}
$networkName = Get-CimInstance -ClassName Win32_NetworkAdapter | Where-Object {$_.PhysicalAdapter -eq "True"} | Sort Index
$networkIP = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object {$_.MACAddress -gt 0} | Sort Index
$networkSummary = New-Object -TypeName 'System.Collections.ArrayList'

foreach($nic in $networkName) {
    $nic_conf = $networkIP | Where-Object {$_.Index -eq $nic.Index}
 
    $networkDetails = New-Object PSObject -Property @{
        Index                = [int]$nic.Index;
        AdapterName         = [string]$nic.NetConnectionID;
        Manufacturer         = [string]$nic.Manufacturer;
        Description          = [string]$nic.Description;
        MACAddress           = [string]$nic.MACAddress;
        IPEnabled            = [bool]$nic_conf.IPEnabled;
        IPAddress            = [string]$nic_conf.IPAddress;
        IPSubnet             = [string]$nic_conf.IPSubnet;
        DefaultGateway       = [string]$nic_conf.DefaultIPGateway;
        DHCPEnabled          = [string]$nic_conf.DHCPEnabled;
        DHCPServer           = [string]$nic_conf.DHCPServer;
        DNSServerSearchOrder = [string]$nic_conf.DNSServerSearchOrder;
    }
    $networkSummary += $networkDetails
}
$NicRawConf = $networkSummary | select AdapterName,IPaddress,IPSubnet,DefaultGateway,DNSServerSearchOrder,MACAddress | Convertto-html -Fragment | select -Skip 1
$NicConf = "<br/><table class=`"table table-bordered table-hover`" >" + $NicRawConf

$RAM = (systeminfo | Select-String 'Total Physical Memory:').ToString().Split(':')[1].Trim()

$ApplicationsFrag = Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | Convertto-html -Fragment | select -skip 1
$ApplicationsTable = "<br/><table class=`"table table-bordered table-hover`" >" + $ApplicationsFrag

$RolesFrag = Get-WindowsFeature | Where-Object {$_.Installed -eq $True} | Select-Object displayname,name  | convertto-html -Fragment | Select-Object -Skip 1
$RolesTable = "<br/><table class=`"table table-bordered table-hover`" >" + $RolesFrag

if($machineType -eq "Physical" -and $ComputerSystemInfo.Manufacturer -match "Dell"){
$DiskLayoutRaw = omreport storage pdisk controller=0 -fmt cdv
$DiskLayoutSemi = $DiskLayoutRaw |  select-string -SimpleMatch "ID,Status," -context 0,($DiskLayoutRaw).Length | convertfrom-csv -Delimiter "," | select Name,Status,Capacity,State,"Bus Protocol","Product ID","Serial No.","Part Number",Media | convertto-html -Fragment
$DiskLayoutTable = "<br/><table class=`"table table-bordered table-hover`" >" + $DiskLayoutsemi

#Try to get RAID layout
$RAIDLayoutRaw = omreport storage vdisk controller=0 -fmt cdv
$RAIDLayoutSemi = $RAIDLayoutRaw |  select-string -SimpleMatch "ID,Status," -context 0,($RAIDLayoutRaw).Length | convertfrom-csv -Delimiter "," | select Name,Status,State,Layout,"Device Name","Read Policy","Write Policy",Media |  convertto-html -Fragment
$RAIDLayoutTable = "<br/><table class=`"table table-bordered table-hover`" >" + $RAIDLayoutsemi
}else {
    $RAIDLayoutTable = "Could not get physical disk info"
    $DiskLayoutTable = "Could not get physical disk info"
}

$HTMLFile = @"
<b>Servername</b>: $ENV:COMPUTERNAME <br>
<b>Server Type</b>: $machineType <br>
<b>Amount of RAM</b>: $RAM <br>
<br>
<h1>NIC Configuration</h1> <br>
$NicConf
<br>
<h1>Installed Applications</h1> <br>
$ApplicationsTable
<br>
<h1>Installed Roles</h1> <br>
$RolesTable
<br>
<h1>Physical Disk information</h1>
$DiskLayoutTable
<h1>RAID information</h1>
$RAIDLayoutTable
"@



$FlexAssetBody = 
@{
    type = 'flexible-assets'
    attributes = @{
            name = $FlexAssetName
            traits = @{
                "name" = $ENV:COMPUTERNAME
                "information" = $HTMLFile
            }
    }
}

#ITGlue upload starts here.
If(Get-Module -ListAvailable -Name "ITGlueAPI") {Import-module ITGlueAPI} Else { install-module ITGlueAPI -Force; import-module ITGlueAPI}
#Settings IT-Glue logon information
Add-ITGlueBaseURI -base_uri $APIEndpoint
Add-ITGlueAPIKey $APIKEy
#Checking if the FlexibleAsset exists. If not, create a new one.
$FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
if(!$FilterID){ 
    $NewFlexAssetData = 
    @{
        type = 'flexible-asset-types'
        attributes = @{
                name = $FlexAssetName
                icon = 'sitemap'
                description = $description
        }
        relationships = @{
            "flexible-asset-fields" = @{
                data = @(
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order           = 1
                            name            = "name"
                            kind            = "Text"
                            required        = $true
                            "show-in-list"  = $true
                            "use-for-title" = $true
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 2
                            name           = "information"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    }
                )
                }
            }
              
       }
New-ITGlueFlexibleAssetTypes -Data $NewFlexAssetData 
$FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
} 

#Upload data to IT-Glue. We try to match the Server name to current computer name.
$ExistingFlexAsset = (Get-ITGlueFlexibleAssets -filter_flexible_asset_type_id $Filterid.id -filter_organization_id $orgID).data | Where-Object {$_.attributes.name -eq $ENV:COMPUTERNAME}

#If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
if(!$ExistingFlexAsset){
$FlexAssetBody.attributes.add('organization-id', $orgID)
$FlexAssetBody.attributes.add('flexible-asset-type-id', $FilterID.id)
Write-Host "Creating new flexible asset"
New-ITGlueFlexibleAssets -data $FlexAssetBody

} else {
Write-Host "Updating Flexible Asset"
Set-ITGlueFlexibleAssets -id $ExistingFlexAsset.id  -data $FlexAssetBody}

And that’s it! Happy PowerShelling! the next chapter will be how to upload the Bitlocker key for a client machine to IT-Glue as a password object, it will also tag all related devices, something that’s pretty cool!

Documenting with PowerShell – New series

Hi All!

Starting this week I’ll be blogging about using PowerShell with your RMM/Automation platform and running scripts to collect valuable documentation. I’ll try to keep it as generic as possible and export the documentation to HTML, but I’ll always include a version to upload it to IT-Glue or Confluence. As requested by some I’ll also include the AMP for N-Central so you can get going with it.

To get started straight away, I’ll share the script that we will be using throughout this series to upload documentation to IT-Glue fully automated. You won’t even need to create flexible assets as the script does this for you.

The Script

For the script you’ll need at least Windows 10, or Server 2012R2+. You’ll also need your IT-Glue API key and the URL, generally speaking that URL is “https://api.itglue.com” or “https://api.eu.itglue.com” for european users. Now let’s get started on our uploading script. 🙂

N-Able users can download the AMP for this script here (Right click->Save as) The script can use Custom Device or Organisation Properties as input, and as thus you can enter the Organisation ID on each Custom Organisation Property and automate your documentation process completely.

#####################################################################
$APIKEy =  "YOUR API KEY GOES HERE"
$APIEndpoint = "https://api.eu.itglue.com"
$orgID = "THE ORGANISATIONID YOU WOULD LIKE TO UPDATE GOES HERE"
$FlexAssetName = "ITGLue AutoDoc - Quick example"
$Description = "a quick overview of easy it is to upload data to IT-Glue"
#####################################################################
#This is the object we'll be sending to IT-Glue. 
$HTMLStuff = @"
<b>Servername</b>: $ENV:COMPUTERNAME <br>
<b>Number of Processors</b>: $ENV:NUMBER_OF_PROCESSORS <br>

This is a little example of how we upload data to IT-Glue.
"@
$FlexAssetBody = 
@{
    type = 'flexible-assets'
    attributes = @{
            name = $FlexAssetName
            traits = @{
                "name" = $ENV:COMPUTERNAME
                "information" = $HTMLStuff
            }
    }
}

#ITGlue upload starts here.
If(Get-Module -ListAvailable -Name "ITGlueAPI") {Import-module ITGlueAPI} Else { install-module ITGlueAPI -Force; import-module ITGlueAPI}
#Settings IT-Glue logon information
Add-ITGlueBaseURI -base_uri $APIEndpoint
Add-ITGlueAPIKey $APIKEy
#Checking if the FlexibleAsset exists. If not, create a new one.
$FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
if(!$FilterID){ 
    $NewFlexAssetData = 
    @{
        type = 'flexible-asset-types'
        attributes = @{
                name = $FlexAssetName
                icon = 'sitemap'
                description = $description
        }
        relationships = @{
            "flexible-asset-fields" = @{
                data = @(
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order           = 1
                            name            = "name"
                            kind            = "Text"
                            required        = $true
                            "show-in-list"  = $true
                            "use-for-title" = $true
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 2
                            name           = "information"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    }
                )
                }
            }
              
       }
New-ITGlueFlexibleAssetTypes -Data $NewFlexAssetData 
$FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
} 

#Upload data to IT-Glue. We try to match the Server name to current computer name.
$ExistingFlexAsset = (Get-ITGlueFlexibleAssets -filter_flexible_asset_type_id $Filterid.id -filter_organization_id $orgID).data | Where-Object {$_.attributes.name -eq $ENV:COMPUTERNAME}

#If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
if(!$ExistingFlexAsset){
$FlexAssetBody.attributes.add('organization-id', $orgID)
$FlexAssetBody.attributes.add('flexible-asset-type-id', $FilterID.id)
Write-Host "Creating new flexible asset"
New-ITGlueFlexibleAssets -data $FlexAssetBody

} else {
Write-Host "Updating Flexible Asset"
Set-ITGlueFlexibleAssets -id $ExistingFlexAsset.id  -data $FlexAssetBody}

The script does multiple things for you, that a lot of other scripts tend to skimp over;

  • We check if a Flexible Asset type with our chosen name is already present, if its not. we create it
  • We then check if a Flexible Asset form already exists with the same name as we’ve entered, if not, we’ll upload a fresh one, if it does, we’ll upload an update for that specific item.

In the following series I’ll teach you how to get the organisation ID by information we gather on the machine you are running your script on. We’ll be tackling how to get the correct devices tagged on your flexible assets, but of course we’ll start by taking apart the script above and teaching you how to create fully automated network documentation.

As always, Happy Powershelling!

Deploying Auto-VPN or Always-On VPN with SSTP

Hi All,

Sorry for the break in blogs about monitoring – I’ve been quite busy with work, so I haven’t had the time to create a monitoring blog. I have been able to create a blog about deploying Always-on VPN, or as Microsoft used to call it “Auto-VPN”. Always-on VPN is going to be the replacement for DirectAccess. DirectAccess was a technology that created 2 hidden VPN tunnels over SSL and encrypted all the data between your client machine and your local network. The downside was that it required Windows Enterprise.

Warning: Long read 🙂
Continue reading

Monitoring with PowerShell Chapter 2: DHCP Pool status

Hi All,

As I’ve explained in my previous the series is taking a bit of a turn here and we’re going to start some blogs about remediation instead of just monitoring. I’ll link back to a previous blog and will explain how we automatically react to these issues within our RMM, if you do not have an RMM – Don’t worry! We’ll include the monitoring + remediation script so you can combine the scripts any way you’d like.

The second monitoring and remediation we’re getting on is a full DHCP-scope and auto-remediate when the scope is completely full. We’ll monitoring several aspects such as the amount of free IP’s, the scope status and lease-time, we’ll also try to clean the scope if it reaches a full state for very old leases or BAD_ADDR’s. Remember that if you bump into this issue a lot it’s better to increase scope size or manage your devices and network 🙂

Continue reading