Category Archives: Automation

Deploying a service principal to (CSP) child tenants

Cloud Solution Providers, or sometimes other types of Managed Service Providers often have to manage a large number of tenants. Ideally, they do their ‘Infrastructure As Code’.

Using various API’s to manage tenants is best done using a Service Principal instead of a user (MFA, lifecycle, etc).

Recently, I was tasked to provide a deployment method of a Service Principal (multi-tenant) to all child tenants of an MSP, including programmatically granting various Graph API permissions. The Graph endpoint for this (oauth2PermissionGrants) is still in Beta, but the other methods I wrote about in the past are not as reliable so we’re using the Beta endpoint.

The linked example script creates an SPN and grants AuditLog.Read.All. If you’re an MSP/CSP, you’ll probably want to capture the tenant ID’s you’re installing into, so you can easily administer these tenants centrally using your main multi-tenant SPN.

Moving forwards, you won’t need an admin user / service account in the tenants you manage anymore, at least for the API’s that support SPN’s.

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/add-servicePrincipalToAllCSPChildTenants.ps1

Note: to completely remove module dependencies / login, check my independent token function.

Upserting Data to Azure SQL DB using PowerShell

For a project involving Azure Security I needed to store fairly large amounts of data in an Azure PaaS database using PowerShell.

If a row already exists, I want to do an UPDATE command, otherwise an INSERT command, also known as an UPSERT in SQL.

It should also use parameters to avoid issues with quotes in fields, and should convert PowerShell null’s/empty objects to the SQL equivalent.

The following function is what resulted, it only supports a single WHERE clause, but should be easy to enhance for those looking to improve 🙂

function invoke-sqlUpsert{
    Param(
        [String]$tableName,
        [Array]$values, # example: @(@{"column"="deviceId";"value"="123415";"dataType"=[Data.SQLDBType]::NVarChar})
        [PSObject]$primaryKey, # example: @{"column"="deviceId";"value"="123415";"dataType"=[Data.SQLDBType]::NVarChar}
        [System.Data.Common.DbConnection]$sqlConn
    )

    $sqlQuery = "BEGIN TRANSACTION;
    UPDATE $tableName
        SET "
    for($i = 0;$i -lt $values.Count;$i++){
        $sqlQuery = "$sqlQuery$($values[$i].column)=@$($values[$i].column)"
        if($i -lt $values.Count-1){
            $sqlQuery = "$sqlQuery,"
        }
    }
    $sqlQuery = "$($sqlQuery)
        WHERE $($primaryKey.column) = @$($primaryKey.column);
    IF @@ROWCOUNT = 0
        BEGIN
            INSERT INTO $tableName ($($primaryKey.column),"
    for($i = 0;$i -lt $values.Count;$i++){
        $sqlQuery = "$sqlQuery$($values[$i].column)"
        if($i -lt $values.Count-1){
            $sqlQuery = "$sqlQuery,"
        }
    }
    $sqlQuery = "$($sqlQuery)
            )
            VALUES (@$($primaryKey.column),"
    for($i = 0;$i -lt $values.Count;$i++){
        $sqlQuery = "$($sqlQuery)@$($values[$i].column)"
        if($i -lt $values.Count-1){
            $sqlQuery = "$sqlQuery,"
        }
    }
    $sqlQuery = "$($sqlQuery));
        END
    COMMIT TRANSACTION;"
           

    $sqlCmd=new-object system.Data.SqlClient.SqlCommand($sqlQuery, $sqlConn)
    $sqlCmd.Parameters.Add((New-OBJECT DATA.SQLClient.SQLParameter("@$($primaryKey.column)",$primaryKey.dataType))) | OUT-NULL
    $sqlCmd.Parameters[0].Value = $primaryKey.value
    for($i = 0;$i -lt $values.Count;$i++){
        $sqlCmd.Parameters.Add((New-OBJECT DATA.SQLClient.SQLParameter("@$($values[$i].column)",$values[$i].dataType))) | OUT-NULL
        $sqlCmd.Parameters[$i+1].Value = $values[$i].value
    }

    for($i=0;$i -lt $sqlCmd.Parameters.count;$i++){
        if($sqlCmd.Parameters[$i].Value -eq $null){
            $sqlCmd.Parameters[$i].Value = [System.DBNull]::Value
        }
    }
    if($sqlCmd.ExecuteNonQuery() -ne 1){
        Throw $_
    }
}

An example of how to connect from an Azure Function before using this command:

using namespace System.Data.SqlClient
using namespace System.Net
$msi_authenticationResult = Invoke-RestMethod -Method Get -Headers @{'Secret' = $env:MSI_SECRET} -Uri ($env:MSI_ENDPOINT +'?resource=https://database.windows.net/&api-version=2017-09-01')
$sqlConn = New-Object System.Data.SqlClient.SqlConnection
$sqlConn.ConnectionString = "Data Source = yourserver.database.windows.net; Initial Catalog = yourdatabase"
$sqlConn.AccessToken = $msi_authenticationResult.access_token
$sqlConn.Open()

Exchange Hybrid lockdown to O365 IP’s only

With the recent Exchange vulnerabilities comes a moment to reflect on further ways to reduce the attach surface of Exchange Servers.

Many organizations still host an Exchange Server solely to maintain a hybrid connectivity link to Office 365. The server therefore has to be publicly accessible, but only to Microsoft. Often this is not the case.

If you don’t have a professional firewall to restrict traffic to only that coming from Microsoft, you can also do so at the IIS level. Microsoft publishes a list of IP’s they use here:

https://endpoints.office.com/endpoints/worldwide

We can then take that source address data and add each IP in it to an Allow entry at the global level in IIS using PowerShell:

$allRanges = @("fe80::946:a60c:3d5:ec11%3","127.0.0.1","::1")
$o365IPs = Invoke-RestMethod -Method GET -UseBasicParsing -Uri "https://endpoints.office.com/endpoints/worldwide" 
$o365IPs | % {$_.ips | %{if($allRanges -notcontains $_){$allRanges += $_}}}
$allRanges | % {
    if($_.IndexOf("/")){
        $payLoad = @{ipAddress=$_.Split("/")[0];allowed="true";subnetMask=$(([ipaddress]([double]4294967296-(1-shl32-$($_.Split("/")[1])))).IPAddressToString);}
    }else{
        $payLoad = @{ipAddress=$_;allowed="true";}
    }
    try{$null = Add-WebConfigurationProperty  -Filter 'system.webServer/security/ipSecurity' -PSPath "IIS:\" -Name "." -Value $payLoad -ErrorAction SilentlyContinue}catch{$Null}
}

Finally, set IIS’s IP Address and Domain restriction mode to Deny:

note: you can add additional ranges to $allRanges as needed for internal management, monitoring etc.

Enable WVD diagnostics for Azure Monitor using ARM Templates

Now that Azure Monitor has support for Windows Virtual Desktop, I wanted to enable this for all WVD workspaces, host pools and application groups.

Christiaan beautifully described how to do this through the Azure Portal, but of course, in an enterprise environment we don’t click around, we define everything as Code!

This example ARM template will enable the Diagnostics setting for the hostpool, workspace and application group vectors. To get data from VM’s (hosts), use your preferred method (plenty of examples exist already and depend on your setup).

After applying / including / redeploying this ARM template in your WVD environment, you’ll be able to see metrics coming in to the Azure Monitor Insights dashboard for WVD 🙂

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/ARM%20templates/WVDDiagnosticSettings.json

Grouping devices in MDATP based on registered users

Microsoft Defender Advanced Threat Protection seems to be becoming the defacto leader in the A/V industry, at least when Windows is concerned, but other OS’es seem to be following quickly 🙂

At one of my international customers, many different locations and departments exist and we’d like to group devices in MDATP based on their primary user so we can assigned different administrators automatically, and apply different web filtering policies.

MDATP has the following options available for grouping:

These membership rules don’t say anything about the user, and the machine domains are all cloud native (no hybrid joins). So we need to use Tags to gain flexible targeting in MDATP.

The following PowerShell script can be scheduled as an Azure Runbook to automatically tag all your MDATP devices based on the ‘Company’ attribute of the device’s primary user. It could also be modified easily to e.g. parse a user’s group membership or UPN’s domain.

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/set-MDATPCustomTags.ps1

If you have a lot of devices, it may take a while for the first run (beyond Azure Automation limits), in that case run it locally first and then schedule it.