Category Archives: Series: PowerShell documenting

Documenting with PowerShell: Handling IT-Glue API security and rate limiting.

I’ve been blogging a whole lot about documentation lately; I truly believe all automated documentation is better than just having people enter data manually. My company uses IT-Glue as a documentation system. IT-Glue is a very cool system but has some huge API limitations. For example; You’re allowed to make 10 requests per second and 10,000 requests per day. These limitations can get pretty bad if you manage a lot of workstations or servers that upload data at the same time.

After my previous blogs the comment I’ve received most was worries about the API key. If they key gets stolen you’re giving away the keys to the castle. The API has no limitations and with a leaked key all your documentation could be download. I’ve been discussing this issue with IT-Glue for some time but haven’t gotten a real solution yet. This has forced me to look for a solution myself. I gave myself some requirements for the solution.

  • The solution needed to be simple and accessible for everyone.
  • The solution needed to have multiple levels of authentication; an API key, IP whitelisting, and organization whitelisting.
  • The solution needed to block requests for all passwords/files/etc for all organisations.
  • The solution needed to allow some form of handling of the API rate limiting, e.g. repeating a request if it was rate limited.
  • The solution needed to be able to used, without adapting any scripts (except URLs and API codes.)

So after some research I decided to use an Azure Function for this. I’ve blogged about Azure Functions before, but the main reason is that running this function in the consumption model will cost us nothing (or next to nothing if you are an extremely heavy user.)

Setup

This time we will not use the Azure Function to only run a script but act as a “middleware” for the IT-Glue API. Follow this guide to set up your Azure function App. The only difference is that we select “PowerShell” as our runtime language. Do not continue at “Create an HTTP triggered function” as we’re going to be inserting our own function.

When the Function App has been deployed click on your Function’s name and then on “platform features”. You should be presented with the following screen

In this screen click on “Configuration” – We’re going to be adding some configuration options here that are used in our scripts. Add the three following items:

  • AzAPIKey: This will be the new API key you will enter on all your scripts that will upload data to IT-Glue. Generate a password for this or enter one of choice.
  • ITGlueURI: This is the current IT-Glue API url you use, most likely https://api.itglue.com or https://api.eu.itglue.com
  • ITGlueAPIKey: Your current API key. This is the only location that this API key will be used from now on.

After this you can return to the overview page and click the + symbol next to the “Functions”, Choose the “HTTP trigger” option. Name the HTTP trigger “AzGlueForwarder” and choose the Anonymous Authorisation level. This is because we are going to take care of authentication on the script level and not at the Azure Function level. After creating the function you’ll be presented with a script page. Paste the following script:

AZGlueForwarder
using namespace System.Net
param($Request, $TriggerMetadata)
#Check if AZapiKey is correct
if ($request.Headers.'x-api-key' -eq $ENV:AzAPIKey) {
    #Comparing the client IP to the Organization list, and checking if it exists.
    $ClientIP = ($request.headers.'X-Forwarded-For' -split ':')[0]
    $CompareList = import-csv "AzGlueForwarder\OrgList.csv" -delimiter ","
    $AllowedOrgs = $comparelist | where-object { $_.ip -eq $ClientIP }
    if (!$AllowedOrgs) { 
        Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
                headers    = @{'content-type' = 'application\json' }
                StatusCode = [httpstatuscode]::OK
                Body       = @{"Error" = "401 - No match found in allowed list" } | convertto-json
            })
        exit 1
    }

    #Sending request to ITGlue
    #$resource = $request.params.path -replace "AzGlueForwarder/", ""
    $resource = $request.url -replace "https://$($ENV:WEBSITE_HOSTNAME)/API/AzGlueForwarder/", ""
    #Replace x-api-key with actual key
    $ITGHeaders = @{
        "x-api-key" = $ENV:ITGlueAPIKey
    } 
    $Method = $($Request.method)
    $ITGBody = $($Request.body)
    #write-host ($AllowedOrgs | out-string)
    $SuccessfullQuery = $false
    $attempt = 3
    while ($attempt -gt 0 -and -not $SuccessfullQuery) {
        try {
            $ITGlueRequest = Invoke-RestMethod -Method $Method -ContentType "application/vnd.api+json" -Uri "$($ENV:ITGlueURI)/$resource" -Body $ITGBody -Headers $ITGHeaders
            $SuccessfullQuery = $true
        }
        catch {
            $ITGlueRequest = @{'Errorcode' = $_.Exception.Response.StatusCode.value__ }
            $rand = get-random -Minimum 0 -Maximum 10
            start-sleep $rand
            $attempt--
            if ($attempt -eq 0) { $ITGlueRequest = @{'Errorcode' = "Error code $($_.Exception.Response.StatusCode.value__) - Made 3 attempts and upload failed. $($_.Exception.Message) " } }
        }
    }

    #Checking if we can strip the data that does not belong to this client. 
    #Important so passwords/items can only be retrieved belonging to this organisation.
    #Can't do it for all requests, such as get-organisation, but for senstive data it works perfectly. :)

    if ($($ITGlueRequest.data.attributes.'organization-id')) {
        write-host ($AllowedOrgs.ITGlueOrgID)
        $ITGlueRequest.data = $ITGlueRequest.data | where-object { $_.attributes.'organization-id' -in $($AllowedOrgs.ITGlueOrgID) }    
    }

    #Sending the final object back to the client.
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
            headers    = @{'content-type' = 'application\json' }
            StatusCode = [httpstatuscode]::OK
            Body       = $ITGlueRequest
        })


}
else {
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
            headers    = @{'content-type' = 'application\json' }
            StatusCode = [httpstatuscode]::OK
            Body       = @{"Error" = "401 - No API Key entered or API key incorrect." } | convertto-json
        })
    
}

Save the script and use the right-hand menu to add a file to the function. Call this file “OrgList.csv”. This is the database that will be used to check which IP’s are allowed to upload data, and for which organisations they can retrieve data.

IP,ITGlueOrgID
1.1.1.1,123456
2.2.2.2,123457

Next click on “Integrate” and select the allowed methods, in our case we want all methods selected for the IT-Glue API. Replace the “Route template” with “{*path}”.

Click on AzGlueForwarder once more and press “Get Function URL” and copy this URL up to the {PATH} part. This will be the URL you will put in place of the API endpoint variable in your scripts. e.g. “https://AzureFunctionITGlue.azurewebsites.net/api/”.

And that’s it! A small recap:

  • Create the Azure Function
  • Add the environment variables AzAPIKey ITGlueBaseURI,ITGlueAPIKey.
  • The function URL will be your new IT-Glue API url to put in your scripts
  • The AzAPIKey is the key to put in your script.
  • The IT-Glue API key will only remain at the Azure Function side.
  • The OrgList.CSV file should contain your client’s their IP’s and allowed organisation.
  • your API requests can only be used for the organisations defined in OrgList.CSV.
  • When an API call fails, the script will try again 3 times, each with a random wait between 1 and 10 seconds to prevent rate limiting from getting in the way.

It’s a fairly simple but clean solution while I try to work with our friends at IT-Glue to increase the API limitations. It also helps on the security side as no one will be able to just download your entire database.

That’s it for today. As always, Happy PowerShelling.

Documenting with PowerShell: Active Directory domain and settings

Clients that still have a server on-site are become rare these days – Most of our client base is either completely public cloud using AAD or they have hosted servers in our private cloud. For these clients I’ve made the following script to document their Active Directory server settings. I always I want to be in complete control of my clients environment. That means having up to date documentation at the ready.

Of course there are a stack of other reasons to document the Active Directory environment, think of disaster recovery/runbook scenarios, troubleshooting, possible mergers, or even simply getting a correct overview of the sites, servers, and roles. So, let’s get started! I’ll be posting 2 versions of the script. One for IT-Glue and another for generic use.

IT-Glue version

Warning: Currently my wordpress installation is still replacing < with the HTML equivalent. I’m looking into better code plugins but please check the HTML code parts if you are seeing strangle results.

#####################################################################
$APIKEy =  "APIKEY"
$orgID = "ORGID"
$APIEndpoint = "https://api.itglue.com"
$FlexAssetName = "Active Directory - AutoDoc"
$Description = "A network one-page document that shows the current configuration for Active Directory."
#####################################################################
#Grabbing ITGlue Module and installing.
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
 
function Get-WinADForestInformation {
    $Data = @{ }
    $ForestInformation = $(Get-ADForest)
    $Data.Forest = $ForestInformation
    $Data.RootDSE = $(Get-ADRootDSE -Properties *)
    $Data.ForestName = $ForestInformation.Name
    $Data.ForestNameDN = $Data.RootDSE.defaultNamingContext
    $Data.Domains = $ForestInformation.Domains
    $Data.ForestInformation = @{
        'Name'                    = $ForestInformation.Name
        'Root Domain'             = $ForestInformation.RootDomain
        'Forest Functional Level' = $ForestInformation.ForestMode
        'Domains Count'           = ($ForestInformation.Domains).Count
        'Sites Count'             = ($ForestInformation.Sites).Count
        'Domains'                 = ($ForestInformation.Domains) -join ", "
        'Sites'                   = ($ForestInformation.Sites) -join ", "
    }
     
    $Data.UPNSuffixes = Invoke-Command -ScriptBlock {
        $UPNSuffixList  =  [PSCustomObject] @{ 
                "Primary UPN" = $ForestInformation.RootDomain
                "UPN Suffixes"   = $ForestInformation.UPNSuffixes -join ","
            }  
        return $UPNSuffixList
    }
     
    $Data.GlobalCatalogs = $ForestInformation.GlobalCatalogs
    $Data.SPNSuffixes = $ForestInformation.SPNSuffixes
     
    $Data.Sites = Invoke-Command -ScriptBlock {
      $Sites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites            
        $SiteData = foreach ($Site in $Sites) {          
          [PSCustomObject] @{ 
                "Site Name" = $site.Name
                "Subnets"   = ($site.Subnets) -join ", "
                "Servers" = ($Site.Servers) -join ", " 
            }  
        }
        Return $SiteData
    }
     
       
    $Data.FSMO = Invoke-Command -ScriptBlock {
        [PSCustomObject] @{ 
            "Domain" = $ForestInformation.RootDomain
            "Role"   = 'Domain Naming Master'
            "Holder" = $ForestInformation.DomainNamingMaster
        }

        [PSCustomObject] @{ 
            "Domain" = $ForestInformation.RootDomain
            "Role"   = 'Schema Master'
            "Holder" = $ForestInformation.SchemaMaster
        }
         
        foreach ($Domain in $ForestInformation.Domains) {
            $DomainFSMO = Get-ADDomain $Domain | Select-Object PDCEmulator, RIDMaster, InfrastructureMaster

            [PSCustomObject] @{ 
                "Domain" = $Domain
                "Role"   = 'PDC Emulator'
                "Holder" = $DomainFSMO.PDCEmulator
            } 

            
            [PSCustomObject] @{ 
                "Domain" = $Domain
                "Role"   = 'Infrastructure Master'
                "Holder" = $DomainFSMO.InfrastructureMaster
            } 

            [PSCustomObject] @{ 
                "Domain" = $Domain
                "Role"   = 'RID Master'
                "Holder" = $DomainFSMO.RIDMaster
            } 

        }
         
        Return $FSMO
    }
     
    $Data.OptionalFeatures = Invoke-Command -ScriptBlock {
        $OptionalFeatures = $(Get-ADOptionalFeature -Filter * )
        $Optional = @{
            'Recycle Bin Enabled'                          = ''
            'Privileged Access Management Feature Enabled' = ''
        }
        ### Fix Optional Features
        foreach ($Feature in $OptionalFeatures) {
            if ($Feature.Name -eq 'Recycle Bin Feature') {
                if ("$($Feature.EnabledScopes)" -eq '') {
                    $Optional.'Recycle Bin Enabled' = $False
                }
                else {
                    $Optional.'Recycle Bin Enabled' = $True
                }
            }
            if ($Feature.Name -eq 'Privileged Access Management Feature') {
                if ("$($Feature.EnabledScopes)" -eq '') {
                    $Optional.'Privileged Access Management Feature Enabled' = $False
                }
                else {
                    $Optional.'Privileged Access Management Feature Enabled' = $True
                }
            }
        }
        return $Optional
        ### Fix optional features
    }
    return $Data
}
 
$TableHeader = "&lt;table class=`"table table-bordered table-hover`" style=`"width:80%`">"
$Whitespace = "&lt;br/>"
$TableStyling = "&lt;th>", "&lt;th style=`"background-color:#4CAF50`">"
 
$RawAD = Get-WinADForestInformation
 
$ForestRawInfo = new-object PSCustomObject -property $RawAD.ForestInformation | convertto-html -Fragment | Select-Object -Skip 1
$ForestNice = $TableHeader + ($ForestRawInfo -replace $TableStyling) + $Whitespace
 
$SiteRawInfo = $RawAD.Sites | Select-Object 'Site Name', Servers, Subnets | ConvertTo-Html -Fragment | Select-Object -Skip 1
$SiteNice = $TableHeader + ($SiteRawInfo -replace $TableStyling) + $Whitespace
 
$OptionalRawFeatures = new-object PSCustomObject -property $RawAD.OptionalFeatures | convertto-html -Fragment | Select-Object -Skip 1
$OptionalNice = $TableHeader + ($OptionalRawFeatures -replace $TableStyling) + $Whitespace
 
$UPNRawFeatures = $RawAD.UPNSuffixes |  convertto-html -Fragment -as list| Select-Object -Skip 1
$UPNNice = $TableHeader + ($UPNRawFeatures -replace $TableStyling) + $Whitespace
 
$DCRawFeatures = $RawAD.GlobalCatalogs | ForEach-Object { Add-Member -InputObject $_ -Type NoteProperty -Name "Domain Controller" -Value $_; $_ } | convertto-html -Fragment | Select-Object -Skip 1
$DCNice = $TableHeader + ($DCRawFeatures -replace $TableStyling) + $Whitespace
 
$FSMORawFeatures = $RawAD.FSMO | convertto-html -Fragment | Select-Object -Skip 1
$FSMONice = $TableHeader + ($FSMORawFeatures -replace $TableStyling) + $Whitespace
 
$ForestFunctionalLevel = $RawAD.RootDSE.forestFunctionality
$DomainFunctionalLevel = $RawAD.RootDSE.domainFunctionality
$domaincontrollerMaxLevel = $RawAD.RootDSE.domainControllerFunctionality
 
$passwordpolicyraw = Get-ADDefaultDomainPasswordPolicy | Select-Object ComplexityEnabled, PasswordHistoryCount, LockoutDuration, LockoutThreshold, MaxPasswordAge, MinPasswordAge | convertto-html -Fragment -As List | Select-Object -skip 1
$passwordpolicyheader = "&lt;tr>&lt;th>&lt;b>Policy&lt;/b>&lt;/th>&lt;th>&lt;b>Setting&lt;/b>&lt;/th>&lt;/tr>"
$passwordpolicyNice = $TableHeader + ($passwordpolicyheader -replace $TableStyling) + ($passwordpolicyraw -replace $TableStyling) + $Whitespace
 
$adminsraw = Get-ADGroupMember "Domain Admins" | Select-Object SamAccountName, Name | convertto-html -Fragment | Select-Object -Skip 1
$adminsnice = $TableHeader + ($adminsraw -replace $TableStyling) + $Whitespace
 
$EnabledUsers = (Get-AdUser -filter * | Where-Object { $_.enabled -eq $true }).count
$DisabledUSers = (Get-AdUser -filter * | Where-Object { $_.enabled -eq $false }).count
$AdminUsers = (Get-ADGroupMember -Identity "Domain Admins").count
$Users = @"
There are &lt;b> $EnabledUsers &lt;/b> users Enabled&lt;br>
There are &lt;b> $DisabledUSers &lt;/b> users Disabled&lt;br>
There are &lt;b> $AdminUsers &lt;/b> Domain Administrator users&lt;br>
"@
 
$FlexAssetBody = @{
    type       = 'flexible-assets'
    attributes = @{
        traits = @{
            'domain-name'               = $RawAD.ForestName
            'forest-summary'            = $ForestNice
            'site-summary'              = $SiteNice
            'domain-controllers'        = $DCNice
            'fsmo-roles'                = $FSMONice
            'optional-features'         = $OptionalNice
            'upn-suffixes'              = $UPNNice
            'default-password-policies' = $passwordpolicyNice
            'domain-admins'             = $adminsnice
            'user-count'                = $Users
        }
    }
}
 
#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            = "Domain Name"
                            kind            = "Text"
                            required        = $true
                            "show-in-list"  = $true
                            "use-for-title" = $true
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 2
                            name           = "Forest Summary"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 3
                            name           = "Site Summary"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 4
                            name           = "Domain Controllers"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 5
                            name           = "FSMO Roles"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 6
                            name           = "Optional Features"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 7
                            name           = "UPN Suffixes"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 8
                            name           = "Default Password Policies"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 9
                            name           = "Domain Admins"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 10
                            name           = "User Count"
                            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.traits.'domain-name' -eq $RawAD.ForestName }
 
#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
} 

this version of the script does the following:

  • It creates a Flexible Asset configuration in IT-Glue called ” Active Directory – AutoDoc”
  • It creates a Flexible Asset file in the supplied organisation($orgID).
  • The flexible asset file will be filled with the domain name, the forest summary, the site summary, domain controller, fsmo roles, optional features, upn suffixes, default password policies, the domain admins, and a user account.

Special thanks in this blog go out to Przemyslaw Klys for his Get-WinADForestInformation function, and to Jon Czerwinski at Chon Consulting Corp for assisting in some layout and ordering.

Generic version
 #Head for HTML file
$head = @"
&lt;Title>Server AD report&lt;/Title>
&lt;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;
}
&lt;/style>
"@

function Get-WinADForestInformation {
    $Data = @{ }
    $ForestInformation = $(Get-ADForest)
    $Data.Forest = $ForestInformation
    $Data.RootDSE = $(Get-ADRootDSE -Properties *)
    $Data.ForestName = $ForestInformation.Name
    $Data.ForestNameDN = $Data.RootDSE.defaultNamingContext
    $Data.Domains = $ForestInformation.Domains
    $Data.ForestInformation = @{
        'Name'                    = $ForestInformation.Name
        'Root Domain'             = $ForestInformation.RootDomain
        'Forest Functional Level' = $ForestInformation.ForestMode
        'Domains Count'           = ($ForestInformation.Domains).Count
        'Sites Count'             = ($ForestInformation.Sites).Count
        'Domains'                 = ($ForestInformation.Domains) -join ", "
        'Sites'                   = ($ForestInformation.Sites) -join ", "
    }
     
    $Data.UPNSuffixes = Invoke-Command -ScriptBlock {
        $UPNSuffixList  =  [PSCustomObject] @{ 
                "Primary UPN" = $ForestInformation.RootDomain
                "UPN Suffixes"   = $ForestInformation.UPNSuffixes -join ","
            }  
        return $UPNSuffixList
    }
     
    $Data.GlobalCatalogs = $ForestInformation.GlobalCatalogs
    $Data.SPNSuffixes = $ForestInformation.SPNSuffixes
     
    $Data.Sites = Invoke-Command -ScriptBlock {
        $Sites = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites            
          $SiteData = foreach ($Site in $Sites) {          
            [PSCustomObject] @{ 
                  "Site Name" = $site.Name
                  "Subnets"   = ($site.Subnets) -join ", "
                  "Servers" = ($Site.Servers) -join ", " 
              }  
          }
          Return $SiteData
      }
     
    $Data.FSMO = Invoke-Command -ScriptBlock {
        [PSCustomObject] @{ 
            "Domain" = $ForestInformation.RootDomain
            "Role"   = 'Domain Naming Master'
            "Holder" = $ForestInformation.DomainNamingMaster
        }

        [PSCustomObject] @{ 
            "Domain" = $ForestInformation.RootDomain
            "Role"   = 'Schema Master'
            "Holder" = $ForestInformation.SchemaMaster
        }
         
        foreach ($Domain in $ForestInformation.Domains) {
            $DomainFSMO = Get-ADDomain $Domain | Select-Object PDCEmulator, RIDMaster, InfrastructureMaster
            [PSCustomObject] @{ 
                "Domain" = $Domain
                "Role"   = 'PDC Emulator'
                "Holder" = $DomainFSMO.PDCEmulator
            } 

            [PSCustomObject] @{ 
                "Domain" = $Domain
                "Role"   = 'PDC Emulator'
                "Holder" = $DomainFSMO.PDCEmulator
            } 

            
            [PSCustomObject] @{ 
                "Domain" = $Domain
                "Role"   = 'Infrastructure Master'
                "Holder" = $DomainFSMO.InfrastructureMaster
            } 

            [PSCustomObject] @{ 
                "Domain" = $Domain
                "Role"   = 'RID Master'
                "Holder" = $DomainFSMO.RIDMaster
            } 

        }
         
        Return $FSMO
    }
     
    $Data.OptionalFeatures = Invoke-Command -ScriptBlock {
        $OptionalFeatures = $(Get-ADOptionalFeature -Filter * )
        $Optional = @{
            'Recycle Bin Enabled'                          = ''
            'Privileged Access Management Feature Enabled' = ''
        }
        ### Fix Optional Features
        foreach ($Feature in $OptionalFeatures) {
            if ($Feature.Name -eq 'Recycle Bin Feature') {
                if ("$($Feature.EnabledScopes)" -eq '') {
                    $Optional.'Recycle Bin Enabled' = $False
                }
                else {
                    $Optional.'Recycle Bin Enabled' = $True
                }
            }
            if ($Feature.Name -eq 'Privileged Access Management Feature') {
                if ("$($Feature.EnabledScopes)" -eq '') {
                    $Optional.'Privileged Access Management Feature Enabled' = $False
                }
                else {
                    $Optional.'Privileged Access Management Feature Enabled' = $True
                }
            }
        }
        return $Optional
        ### Fix optional features
    }
    return $Data
}
 
$TableHeader = "&lt;table class=`"table table-bordered table-hover`" style=`"width:80%`">"
$Whitespace = "&lt;br/>"
$TableStyling = "&lt;th>", "&lt;th style=`"background-color:#4CAF50`">"
 
$RawAD = Get-WinADForestInformation
 
$ForestRawInfo = new-object PSCustomObject -property $RawAD.ForestInformation | convertto-html -Fragment | Select-Object -Skip 1
$ForestNice = $TableHeader + ($ForestRawInfo -replace $TableStyling) + $Whitespace
 
$SiteRawInfo = $RawAD.Sites | Select-Object 'Site Name', Servers, Subnets | ConvertTo-Html -Fragment | Select-Object -Skip 1
$SiteNice = $TableHeader + ($SiteRawInfo -replace $TableStyling) + $Whitespace
 
$OptionalRawFeatures = new-object PSCustomObject -property $RawAD.OptionalFeatures | convertto-html -Fragment | Select-Object -Skip 1
$OptionalNice = $TableHeader + ($OptionalRawFeatures -replace $TableStyling) + $Whitespace
 
$UPNRawFeatures = $RawAD.UPNSuffixes | convertto-html -Fragment | Select-Object -Skip 1
$UPNNice = $TableHeader + ($UPNRawFeatures -replace $TableStyling) + $Whitespace
 
$DCRawFeatures = $RawAD.GlobalCatalogs | ForEach-Object { Add-Member -InputObject $_ -Type NoteProperty -Name "Domain Controller" -Value $_; $_ } | convertto-html -Fragment | Select-Object -Skip 1
$DCNice = $TableHeader + ($DCRawFeatures -replace $TableStyling) + $Whitespace
 
$FSMORawFeatures = $RawAD.FSMO | convertto-html -Fragment | Select-Object -Skip 1
$FSMONice = $TableHeader + ($FSMORawFeatures -replace $TableStyling) + $Whitespace
 
$ForestFunctionalLevel = $RawAD.RootDSE.forestFunctionality
$DomainFunctionalLevel = $RawAD.RootDSE.domainFunctionality
$domaincontrollerMaxLevel = $RawAD.RootDSE.domainControllerFunctionality
 
$passwordpolicyraw = Get-ADDefaultDomainPasswordPolicy | Select-Object ComplexityEnabled, PasswordHistoryCount, LockoutDuration, LockoutThreshold, MaxPasswordAge, MinPasswordAge | convertto-html -Fragment -As List | Select-Object -skip 1
$passwordpolicyheader = "&lt;tr>&lt;th>&lt;b>Policy&lt;/b>&lt;/th>&lt;th>&lt;b>Setting&lt;/b>&lt;/th>&lt;/tr>"
$passwordpolicyNice = $TableHeader + ($passwordpolicyheader -replace $TableStyling) + ($passwordpolicyraw -replace $TableStyling) + $Whitespace
 
$adminsraw = Get-ADGroupMember "Domain Admins" | Select-Object SamAccountName, Name | convertto-html -Fragment | Select-Object -Skip 1
$adminsnice = $TableHeader + ($adminsraw -replace $TableStyling) + $Whitespace
 
$EnabledUsers = (Get-AdUser -filter * | Where-Object { $_.enabled -eq $true }).count
$DisabledUSers = (Get-AdUser -filter * | Where-Object { $_.enabled -eq $false }).count
$AdminUsers = (Get-ADGroupMember -Identity "Domain Admins").count
$Users = @"
There are &lt;b> $EnabledUsers &lt;/b> users Enabled&lt;br>
There are &lt;b> $DisabledUSers &lt;/b> users Disabled&lt;br>
There are &lt;b> $AdminUsers &lt;/b> Domain Administrator users&lt;br>
"@

$HTMLFile = @"
$head
&lt;b>Domain Name&lt;/b>: $($RawAD.ForestName) &lt;br>
&lt;br>
&lt;h1>Forest Configuration&lt;/h1> &lt;br>
$ForestNice
&lt;br>
&lt;h1>Site Summary&lt;/h1> &lt;br>
$SiteNice
&lt;br>
&lt;h1>Domain Controllers&lt;/h1> &lt;br>
$DCNice
&lt;br>
&lt;h1>FSMO Roles&lt;/h1>
$FSMONice
&lt;h1>Optional Features&lt;/h1>
$OptionalNice
&lt;br>
&lt;h1>UPN Suffixes&lt;/h1>
$UPNNice
&lt;br>
&lt;h1>Password Policies&lt;/h1>
$passwordpolicyNice
&lt;br>
&lt;h1>Domain Admins&lt;/h1>
$adminsnice
&lt;br>
&lt;h1>Domain Admins&lt;/h1>
$Users
&lt;br>
"@
$HTMLFile | out-file C:\Temp\ServerDoc.html 

And that’s it. If you want more local documentation without the IT-Glue component you should check out this blog too. as always, Happy PowerShelling.

Update: After some comments from Przemysław Kłys I’ve updated the scripts to be a little more efficient and mostly prettier 🙂

Documenting with PowerShell: Documenting SQL settings and databases

Most of our clients have some form of line of business application that requires a database engine. in 99% of the cases this ends up being a SQL server. I always enjoy being in complete control of an environment so whenever we deploy SQL servers we automatically run this documentation script. This is especially good if you ever need to recreate databases, or need to check what the state of a SQL server was a couple of weeks ago.

So, for this script we use the SQLPS module which is included on any server with SQL Server 2012+ installed. The SQLPS module gives us a PSDrive with the SQLSERVER:\ path. This allows us to grab all information we need.

The script documents the existing databases, their settings, file locations, but also generic server settings. It automatically finds all instances on the server so in the case of multiple SQL instances you’re also covered by this script 🙂

IT-Glue script
#####################################################################
$APIKEy = "ITGLUEAPIKEY"
$APIEndpoint = "https://api.eu.itglue.com"
$orgID = "ORGIDHERE"
$FlexAssetName = "ITGLue AutoDoc - SQL Server"
$Description = "SQL Server settings and configuration, Including databases."
#####################################################################
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
import-module SQLPS
$Instances = Get-ChildItem "SQLSERVER:\SQL\$($ENV:COMPUTERNAME)"
foreach ($Instance in $Instances) {
    $databaseList = get-childitem "SQLSERVER:\SQL\$($ENV:COMPUTERNAME)\$($Instance.Displayname)\Databases"
    $Databases = @()
    foreach ($Database in $databaselist) {
        $Databaseobj = New-Object -TypeName PSObject
        $Databaseobj | Add-Member -MemberType NoteProperty -Name "Name" -value $Database.Name
        $Databaseobj | Add-Member -MemberType NoteProperty -Name "Status" -value $Database.status
        $Databaseobj | Add-Member -MemberType NoteProperty -Name  "RecoveryModel" -value $Database.RecoveryModel
        $Databaseobj | Add-Member -MemberType NoteProperty -Name  "LastBackupDate" -value $Database.LastBackupDate
        $Databaseobj | Add-Member -MemberType NoteProperty -Name  "DatabaseFiles" -value $database.filegroups.files.filename
        $Databaseobj | Add-Member -MemberType NoteProperty -Name  "Logfiles"      -value $database.LogFiles.filename
        $Databaseobj | Add-Member -MemberType NoteProperty -Name  "MaxSize" -value $database.filegroups.files.MaxSize
        $Databases += $Databaseobj
    }
    $InstanceInfo = $Instance | Select-Object DisplayName, Collation, AuditLevel, BackupDirectory, DefaultFile, DefaultLog, Edition, ErrorLogPath | convertto-html -PreContent "&lt;h1>Settings&lt;/h1>" -Fragment | Out-String
    $Instanceinfo = $instanceinfo -replace "&lt;th>", "&lt;th style=`"background-color:#4CAF50`">"
    $InstanceInfo = $InstanceInfo -replace "&lt;table>", "&lt;table class=`"table table-bordered table-hover`" style=`"width:80%`">"
    $DatabasesHTML = $Databases | ConvertTo-Html -fragment -PreContent "&lt;h3>Database Settings&lt;/h3>" | Out-String
    $DatabasesHTML = $DatabasesHTML -replace "&lt;th>", "&lt;th style=`"background-color:#4CAF50`">"
    $DatabasesHTML = $DatabasesHTML -replace "&lt;table>", "&lt;table class=`"table table-bordered table-hover`" style=`"width:80%`">"



    #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 = @{
                "instance-name"     = "$($ENV:COMPUTERNAME)\$($Instance.displayname)"
                "instance-settings" = $InstanceInfo
                "databases"         = $DatabasesHTML
                "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            = "Instance Name"
                                kind            = "Text"
                                required        = $true
                                "show-in-list"  = $true
                                "use-for-title" = $true
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 2
                                name           = "Instance Settings"
                                kind           = "Textbox"
                                required       = $false
                                "show-in-list" = $true
                            }
                        },
                        @{
                            type       = "flexible_asset_fields"
                            attributes = @{
                                order          = 3
                                name           = "Databases"
                                kind           = "Textbox"
                                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.traits.'instance-name' -eq "$($ENV:COMPUTERNAME)\$($Instance.displayname)" }
    #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
    }
}
Generic version

As always I’ve included a generic version. You can use this with any other system.

import-module SQLPS
$Instances = Get-ChildItem "SQLSERVER:\SQL\$($ENV:COMPUTERNAME)"
foreach ($Instance in $Instances) {
    $InstanceInfo = $Instance | Select-Object DisplayName, Collation, AuditLevel, BackupDirectory, DefaultFile, DefaultLog, Edition, ErrorLogPath
    $databaseList = get-childitem "SQLSERVER:\SQL\$($ENV:COMPUTERNAME)\$($Instance.Displayname)\Databases"
    $Databases = @()
    foreach($Database in $databaselist){
        $Databaseobj = New-Object -TypeName PSObject
        $Databaseobj | Add-Member -MemberType NoteProperty -Name "Name" -value $Database.Name
        $Databaseobj | Add-Member -MemberType NoteProperty -Name "Status" -value $Database.status
        $Databaseobj | Add-Member -MemberType NoteProperty -Name  "RecoveryModel" -value $Database.RecoveryModel
        $Databaseobj | Add-Member -MemberType NoteProperty -Name  "LastBackupDate" -value $Database.LastBackupDate
        $Databaseobj | Add-Member -MemberType NoteProperty -Name  "DatabaseFiles" -value $database.filegroups.files.filename
        $Databaseobj | Add-Member -MemberType NoteProperty -Name  "Logfiles"      -value $database.LogFiles.filename
        $Databaseobj | Add-Member -MemberType NoteProperty -Name  "MaxSize" -value $database.filegroups.files.MaxSize
        $Databases += $Databaseobj
    }
    $InstanceInfo = $Instance | Select-Object DisplayName, Collation, AuditLevel, BackupDirectory, DefaultFile, DefaultLog, Edition, ErrorLogPath | convertto-html -PreContent "&lt;h1>Settings&lt;/h1>" -Fragment | Out-String
    $Instanceinfo = $instanceinfo -replace "&lt;th>", "&lt;th style=`"background-color:#4CAF50`">"
    $InstanceInfo = $InstanceInfo -replace "&lt;table>", "&lt;table class=`"table table-bordered table-hover`" style=`"width:80%`">"
    $DatabasesHTML = $Databases | ConvertTo-Html -fragment -PreContent "&lt;h3>Database Settings&lt;/h3>" | Out-String
    $DatabasesHTML = $DatabasesHTML -replace "&lt;th>", "&lt;th style=`"background-color:#4CAF50`">"
    $DatabasesHTML = $DatabasesHTML -replace "&lt;table>", "&lt;table class=`"table table-bordered table-hover`" style=`"width:80%`">"

    $output = $InstanceInfo,$DatabasesHTML | out-file "C:\Temp\Output.html"

}

a small warning: it seems that the latest wordpress updates makes the < symbol appear as its html encoded version in the code. Visual Studio Code automatically converts this. if you are using any other IDE replace this yourself.

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

Monitoring and Documenting with PowerShell: End of year review

Hi! So this is the final post of this year. I’m going to be enjoying some well deserved holidays and spend Christmas with my family. The past year has been pretty cool. I’ve been doing so many cool projects.

I figured I also would list the top blogs of this year by views, and just generally some stuff I’m proud of, so lets get started:

Top blogs

The most viewed blog this year is my Functional PowerShell for MSPs webinar, which is pretty amazing because it was only posted 3 months ago. I still see the views racking up on the Teams Live Event recording and I am going to be giving another (albeit slightly shorter one) the 16th of december.

The runner up in this is the start of the Documenting with PowerShell series. That entire series seems to have been a favorite for most people. The third place is going to the unofficial IT-Glue backup script.

My personal favorite has to be a recent blog; either the Secure Application Model blog or the OneDrive monitoring script which uses user impersonation.

Documenting with PowerShell series

The documenting with PowerShell series has been a hit. I’ve taken a small break from it to reorganise and make it a little more “Eye-candy” focussed as this was the primary question I’ve been getting. I love how some of you have adapted the scripts. Most of them were made as an example so it’s cool to see all different variations of it. I will be continuing this series at the start of next year. If you have any wishes, let me know!

Monitoring with PowerShell series

The monitoring with PowerShell series is still my baby, I love doing it and showing all the different methods of using PowerShell over SNMP, or PowerShell over generic WMI monitoring. Next year I hope to still be posting at least 1 blog a week. I’ve recently been mailed some question that I will be picking up next year too.

Special thanks

This year was great! I especially want to thank my peers in MSP’r’Us that always help me find new ideas. I also want to thank Datto for the way we have been collaborating and adding my blogs and script to their product. It’s been a great adventure.

And that’s it! I wish all my readers an amazing Christmas, and of course a happy new year. As always, Happy PowerShelling!

Documenting with PowerShell: Documenting Office365 mailbox permissions

I like being able to report to our clients exactly what the permissions on mailboxes are. The only issue with reporting via the Office 365 admin portal is that we don’t get a history. I’ve decided to make sure that I can do this by documenting the permissions daily.

To do this, I have two versions of the script; one for IT-Glue and one for general HTML file usage. Both scripts connect to the Office365 Exchange PowerShell using the Secure App Model. For more information you can check this blog post.

IT-Glue version

The IT-Glue version gets all contacts in your IT-Glue environment, compares these to the domains found in each of your partner tenants and documents the permissions for the right tenant into the correct IT-Glue client. The script creates a Flexible Asset for you if it does not yet exist.

$key                   = "YOUR IT Glue Key here"
$ApplicationId         = 'Application ID'
$ApplicationSecret     = 'Application Secret' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'YourTenantID'
$RefreshToken          = 'YourRefreshTokens'
$ExchangeRefreshToken  = 'YourExchangeRefresherToken'
$upn                   = 'UPN-That-Generated-Tokens'
$APIEndpoint           = "https://api.eu.itglue.com"
$FlexAssetName         = "Office365 Permissions - Automatic Documentation"
#######################################################################
write-output "Getting Module"
If (Get-Module -ListAvailable -Name "ITGlueAPI") { Import-module ITGlueAPI } Else { install-module ITGlueAPI -Force; import-module ITGlueAPI }
If (Get-Module -ListAvailable -Name "MsOnline") { Import-module "Msonline" } Else { install-module "MsOnline" -Force; import-module "Msonline" }
If (Get-Module -ListAvailable -Name "PartnerCenter") { Import-module "PartnerCenter" } Else { install-module "PartnerCenter" -Force; import-module "PartnerCenter" }
Write-Output "Generating tokens to login"
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
Add-ITGlueBaseURI -base_uri $APIEndpoint
Add-ITGlueAPIKey $key
write-output "Getting IT-Glue contact list"
$i = 0
do {
    $AllITGlueContacts += (Get-ITGlueContacts -page_size 1000 -page_number $i).data.attributes
    $i++
    Write-Host "Retrieved $($AllITGlueContacts.count) Contacts" -ForegroundColor Yellow
}while ($AllITGlueContacts.count % 1000 -eq 0 -and $AllITGlueContacts.count -ne 0) 
Write-Output "Checking if flexible asset exists."
$FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
if (!$FilterID) { 
Write-Output "No Flexible Asset found. Creating new one"
    $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            = "tenantid"
                        kind            = "Text"
                        required        = $true
                        "show-in-list"  = $true
                        "use-for-title" = $true
                    }
                },
                @{
                    type       = "flexible_asset_fields"
                    attributes = @{
                        order          = 2
                        name           = "permissions"
                        kind           = "Textbox"
                        required       = $false
                        "show-in-list" = $true
                    }
                },
                @{
                    type       = "flexible_asset_fields"
                    attributes = @{
                        order          = 3
                        name           = "tenant-name"
                        kind           = "Text"
                        required       = $false
                        "show-in-list" = $false
                    }
                }
            )
            }
        }
          
   }
    New-ITGlueFlexibleAssetTypes -Data $NewFlexAssetData 
    $FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
}


write-output "Logging in."
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All 
   
foreach ($customer in $customers) {
    $MSOLPrimaryDomain = (get-msoldomain -TenantId $customer.tenantid | Where-Object { $_.IsInitial -eq $false }).name
    $customerDomains = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.status -contains "Verified" }
    $MSOLtentantID = $customer.tenantid
    #Connecting to the O365 tenant
    $InitialDomain = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.IsInitial -eq $true }
    Write-host "Documenting $($Customer.Name)"
    $token = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716'-RefreshToken $ExchangeRefreshToken -Scopes 'https://outlook.office365.com/.default' -Tenant $customer.TenantId
    $tokenValue = ConvertTo-SecureString "Bearer $($token.AccessToken)" -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $customerId = $customer.DefaultDomainName
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($customerId)&amp;BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection
    Import-PSSession $session -CommandName "Get-MailboxPermission", "Get-Mailbox" -AllowClobber
    $Mailboxes = Get-Mailbox -ResultSize:Unlimited | Sort-Object displayname

    foreach ($mailbox in $mailboxes) {
        $AccesPermissions = Get-MailboxPermission -Identity $mailbox.identity | Where-Object { $_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false } -erroraction silentlycontinue | Select-Object User, accessrights
        if ($AccesPermissions) { $HTMLPermissions += $AccesPermissions | convertto-html -frag -PreContent "<h4>Permissions on $($mailbox.PrimarySmtpAddress)</h4>" | Out-String }
    }
    Remove-PSSession $session

    $FlexAssetBody =
    @{
        type       = 'flexible-assets'
        attributes = @{
            traits = @{
                'permissions' = $HTMLPermissions
                'tenantid'    = $MSOLtentantID
                'tenant-name' = $initialdomain.name
            }
        }
    }


    write-output "             Finding $($customer.name) in IT-Glue"
    $orgID = @()
    foreach ($customerDomain in $customerdomains) {
        $orgID += ($AllITGlueContacts | Where-Object { $_.'contact-emails'.value -match $customerDomain.name }).'organization-id' | Select-Object -Unique

    }



    write-output "             Uploading Office Permission $($customer.name) into IT-Glue"
    foreach ($org in $orgID) {
        $ExistingFlexAsset = (Get-ITGlueFlexibleAssets -filter_flexible_asset_type_id $($filterID.ID) -filter_organization_id $org).data | Where-Object { $_.attributes.traits.'tenant-name' -eq $initialdomain.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', $org)
            $FlexAssetBody.attributes.add('flexible-asset-type-id', $($filterID.ID))
            write-output "                      Creating new Office Permission $($customer.name) into IT-Glue organisation $org"
            New-ITGlueFlexibleAssets -data $FlexAssetBody
        }
        else {
            write-output "                      Updating Office Permission $($customer.name) into IT-Glue organisation $org"
            $ExistingFlexAsset = $ExistingFlexAsset[-1]
            Set-ITGlueFlexibleAssets -id $ExistingFlexAsset.id  -data $FlexAssetBody
        }

    }
    $MSOLPrimaryDomain = $null
    $MSOLtentantID = $null
    $AccesPermissions = $null
    $HTMLPermissions = $null 
}

General HTML version

The generic HTML version makes a HTML file per client in C:\Temp. It uses the name of the client as the filename.

$ApplicationId         = 'ApplicationID'
$ApplicationSecret     = 'ApplicationSecret' | Convertto-SecureString -AsPlainText -Force
$TenantID              = 'YourTenantID'
$RefreshToken          = 'RefreshToken'
$ExchangeRefreshToken  = 'ExchangeRefreshToken'
$upn                   = 'YourUPN'
#######################################################################
write-output "Getting Modules"
If (Get-Module -ListAvailable -Name "MsOnline") { Import-module "Msonline" } Else { install-module "MsOnline" -Force; import-module "Msonline" }
If (Get-Module -ListAvailable -Name "PartnerCenter") { Import-module "PartnerCenter" } Else { install-module "PartnerCenter" -Force; import-module "PartnerCenter" }
Write-Output "Generating tokens to login"
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 
write-output "Logging in."
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All 
$head = @"
&lt;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');
  }&lt;/script>
&lt;title>Audit Log Report&lt;/title>
&lt;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 */
}
&lt;/style>
"@
foreach ($customer in $customers) {
    $MSOLPrimaryDomain = (get-msoldomain -TenantId $customer.tenantid | Where-Object { $_.IsInitial -eq $false }).name
    $customerDomains = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.status -contains "Verified" }
    $MSOLtentantID = $customer.tenantid
    #Connecting to the O365 tenant
    $InitialDomain = Get-MsolDomain -TenantId $customer.TenantId | Where-Object { $_.IsInitial -eq $true }
    Write-host "Documenting $($Customer.Name)"
    $token = New-PartnerAccessToken -ApplicationId 'a0c73c16-a7e3-4564-9a95-2bdf47383716'-RefreshToken $ExchangeRefreshToken -Scopes 'https://outlook.office365.com/.default' -Tenant $customer.TenantId
    $tokenValue = ConvertTo-SecureString "Bearer $($token.AccessToken)" -AsPlainText -Force
    $credential = New-Object System.Management.Automation.PSCredential($upn, $tokenValue)
    $customerId = $customer.DefaultDomainName
    $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$($customerId)&amp;BasicAuthToOAuthConversion=true" -Credential $credential -Authentication Basic -AllowRedirection
    Import-PSSession $session -CommandName "Get-MailboxPermission", "Get-Mailbox" -AllowClobber
    $Mailboxes = Get-Mailbox -ResultSize:Unlimited | Sort-Object displayname
    $MSOLPrimaryDomain = $null
    $MSOLtentantID = $null
    $AccesPermissions = $null
    $HTMLPermissions = $null
    foreach ($mailbox in $mailboxes) {
        $AccesPermissions = Get-MailboxPermission -Identity $mailbox.identity | Where-Object { $_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false } -erroraction silentlycontinue | Select-Object User, accessrights
        if ($AccesPermissions) { $HTMLPermissions += $AccesPermissions | convertto-html -frag -PreContent "&lt;h4>Permissions on $($mailbox.PrimarySmtpAddress)&lt;/h4>" | Out-String }
    }
    Remove-PSSession $session
    $CompleteHTML = $head,$HTMLPermissions | Out-String | out-file "C:\Temp\$($customer.name).html"
} 

Monitoring with PowerShell: Monitoring failed logins for Office365

So this was another request by a reader; he has MFA configured for all his users, but still wants to know when the failed logon count increases. Mostly so he can warn his users that a possible spear-phising attempt might also be imminent. We know that when brute force does not work, focussed bad actors will often try the next avenue of attack.

At his request i’ve made the following scripts, one will monitor all possible locations using your partner credentials. The other will monitor only one tenant. I personally like the latter better as I’ve integrated this into my RMM so it can run and alert per client, Also it’s a little faster.

The Script

The script is designed to run at least every 4 hours, but can be run even on a 5-10 minute basis. It will get all info for the previous 4 hours, If you want to decrease on increase this you can edit line 13. Getting the logs is based on Elliot’s script to get the unified logs here. To connect with MFA, use my other blog here to generate your Secure App Model credentials.

Get Failed Logon information for all tenants

$credential = Get-Credential
Connect-MsolService -Credential $credential
$customers = Get-msolpartnercontract -All
$FilteredLogs = @()
$FailedLogonCount = 0
foreach ($customer in $customers) {
    $InitDomain = $customer.DefaultDomainName
    $DelegatedOrgURL = "https://outlook.office365.com/powershell-liveid?DelegatedOrg=" + $InitDomain
    write-host "Connecting to $($customer.Name) Security Center"
    $s = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection
    Import-PSSession $s -CommandName Search-UnifiedAuditLog -AllowClobber
    write-host "Getting last 30 minutes of logs for $($customer.Name)"
    $startDate = (Get-Date).addhours(-4)
    $endDate = (Get-Date)
    $Logs = @()
    Write-Host "Retrieving logs for $($customer.name)" -ForegroundColor Blue
    do {
        $logs += Search-unifiedAuditLog -SessionCommand ReturnLargeSet -SessionId $customer.name -ResultSize 5000 -StartDate $startDate -EndDate $endDate -Operations userloginfailed #-SessionId "$($customer.name)"
        Write-Host "Retrieved $($logs.count) logs" -ForegroundColor Yellow
    }while ($Logs.count % 5000 -eq 0 -and $logs.count -ne 0)
   $FilteredLogs +=  $logs.auditdata | convertfrom-json -ErrorAction SilentlyContinue | Select-Object UserID, ClientIP | Group-Object -Property UserID
   Foreach($item in $FilteredLogs){
       if($item.name -ne $null){ $FailedLogonCount += $item.count; $FailedLogon += "$($Item.name) has $($item.count) failed logons from the following IPs: $($Item.group.ClientIP) `n" }
   }
}

if(!$FailedLogonCount){ $FailedLogon = "Healthy"}

Get failed logins for only one tenant

$TenantName = "TenantDomain.onmicrosoft.com"

$credential = Get-Credential
Connect-MsolService -Credential $credential
$FilteredLogs = @()
$FailedLogonCount = 0
$customer = Get-msolpartnercontract | Where-Object {$_.DefaultDomainName -eq $TenantName}

$InitDomain = $customer.DefaultDomainName
    $DelegatedOrgURL = "https://outlook.office365.com/powershell-liveid?DelegatedOrg=" + $InitDomain
    write-host "Connecting to $($customer.Name) Security Center"
    $s = New-PSSession -ConnectionUri $DelegatedOrgURL -Credential $credential -Authentication Basic -ConfigurationName Microsoft.Exchange -AllowRedirection
    Import-PSSession $s -CommandName Search-UnifiedAuditLog -AllowClobber
    write-host "Getting last 30 minutes of logs for $($customer.Name)"
    $startDate = (Get-Date).addhours(-4)
    $endDate = (Get-Date)
    $Logs = @()
    Write-Host "Retrieving logs for $($customer.name)" -ForegroundColor Blue
    do {
        $logs += Search-unifiedAuditLog -SessionCommand ReturnLargeSet -SessionId $customer.name -ResultSize 5000 -StartDate $startDate -EndDate $endDate -Operations userloginfailed #-SessionId "$($customer.name)"
        Write-Host "Retrieved $($logs.count) logs" -ForegroundColor Yellow
    }while ($Logs.count % 5000 -eq 0 -and $logs.count -ne 0)
   $FilteredLogs +=  $logs.auditdata | convertfrom-json -ErrorAction SilentlyContinue | Select-Object UserID, ClientIP | Group-Object -Property UserID
   Foreach($item in $FilteredLogs){
       if($item.name -ne $null){ $FailedLogonCount += $item.count; $FailedLogon += "$($Item.name) has $($item.count) failed logons from the following IPs: $($Item.group.ClientIP) `n" }
   }


if(!$FailedLogonCount){ $FailedLogon = "Healthy"}

You can choose to alert just on the FailedLogon variable, or alert based on the actual count via FailedLogonCount. As always, Happy Powershelling.

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!

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

 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 -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
$PermCSV = $Permissions | ConvertTo-Csv -Delimiter "," | out-file "C:\Export\ExportOfPermissions.csv" -append
}

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!