10 MIN READ

Balance Device Wave Groups for granular Intune deployments

As the CrowdStrike outage winds down and things return to normal for most users, now is a great time to think about update waves! If you’re using Autopatch, Microsoft has you covered for the most part. But if you’re not, this could be an interesting read for you!

We’ve developed a C# Function App designed to balance devices from a main group into various Wave groups. This app can run on a recurring schedule, rebalancing devices as needed when the number of devices changes due to growth or shrinkage.

Deploying your Windows update policies, Defender for Endpoint, or other configurations to these groups can help minimize the impact of any issues that arise with patches or other deployments.

Currently, only managed Windows and Mac Intune devices are balanced across the Wave groups.

We also offer a more advanced version of this Function App, which includes:

  • Key Vault Integration for App ID and Secrets: Securely manage your application credentials.
  • Multi-Tenant Configuration with Central Config: Manage multiple tenants with a centralized configuration.
  • Exclude Stale Devices from Waves: Automatically exclude devices that are no longer active.

Contact us to learn more about these advanced features and how they can help secure your organization.

using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Graph.Models;

namespace FunctionAppExample
{
    public static class ManageDeviceGroups
    {
        [Function("TimerTrigger_Weekday_7AM")]
        public static void Run([TimerTrigger("0 0 7 * * 1-5")] TimerInfo myTimer, ILogger log)
        {
            log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
            //use this for time trigger, use http for testing
        }

            [Function("ManageDeviceGroups")]
        public static async Task<string> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req,
            FunctionContext context)
        {
            var log = context.GetLogger("ManageDeviceGroups");
            log.LogInformation("C# HTTP trigger function processed a request.");

            var waves = new Dictionary<int, wave>();

            //START CONFIG
            var tenantid = "";
            var appid = "";
            var secret = "";

            var maindevicegroup = "Baseline - Modern Workplace Devices";

            var wave1 = new wave();
            wave1.Id = 1;
            wave1.Name = "Baseline - Devices Wave 1";
            wave1.percentagegoal = 0.20f;
            waves.Add(1, wave1);

            var wave2 = new wave();
            wave2.Id = 2;
            wave2.Name = "Baseline - Devices Wave 2";
            wave2.percentagegoal = 0.30f;
            waves.Add(2, wave2);

            var wave3 = new wave();
            wave3.Id = 3;
            wave3.Name = "Baseline - Devices Wave 3";
            wave3.percentagegoal = 0.50f;
            waves.Add(3, wave3);
            //END CONFIG

            // Create a ClientSecretCredential instance
            var credential = new ClientSecretCredential(tenantid, appid, secret);

            // Create a GraphServiceClient instance with the credential
            var graphClient = new GraphServiceClient(credential);
            

            var moderndevicegroup = await graphClient.Groups.GetAsync((requestConfiguration) =>
            {
                requestConfiguration.QueryParameters.Count = true;
                requestConfiguration.QueryParameters.Filter = $"displayname eq '{maindevicegroup}'";
                requestConfiguration.QueryParameters.Select = new string[] { "id", "displayName" };
                requestConfiguration.Headers.Add("ConsistencyLevel", "eventual");
            });

            // Retrieve the devices from the source group
            var devices = await graphClient.Groups[moderndevicegroup.Value[0].Id].Members.GetAsync();

            var manageddevices = new List<Microsoft.Graph.Models.Device>();

            foreach (var device in devices.Value) {
                var managedDevice = device as Microsoft.Graph.Models.Device;
                if (!string.IsNullOrEmpty(managedDevice.ManagementType)) {
                    if (managedDevice != null && managedDevice.ManagementType.Contains("MDM"))
                    {
                        string os = managedDevice.OperatingSystem;
                        if (os.ToLower().Contains("windows") || os.ToLower().Contains("mac"))
                        {
                            manageddevices.Add(managedDevice);
                        }
                    }
                }
            }

            foreach (var wave in waves)
            {
                var wavegroup = await graphClient.Groups.GetAsync((requestConfiguration) =>
                {
                    requestConfiguration.QueryParameters.Count = true;
                    requestConfiguration.QueryParameters.Filter = $"displayName eq '{wave.Value.Name}'";
                    requestConfiguration.QueryParameters.Select = new string[] { "id", "displayName"};
                    requestConfiguration.Headers.Add("ConsistencyLevel", "eventual");
                });

                //Update the wave
                wave.Value.entraid = wavegroup.Value[0].Id;
                await wave.Value.updatewaveAsync(graphClient, manageddevices.Count());
            }

            foreach (var wave in waves)
            {
                if (wave.Value.change == false)
                {
                    continue;
                }

                var difference = wave.Value.countgoal - wave.Value.count;
                if (difference < 0)
                {
                    //need to remove from this wave
                    foreach (var device in wave.Value.devices)
                    {
                        if (difference == 0)
                        {
                            break;
                        }
                        try
                        {
                            await graphClient.Groups[wave.Value.entraid].Members[device.Id].Ref.DeleteAsync();
                        }
                        catch (Exception e)
                        {
                            log.LogError(e.ToString());
                        }
                        difference++;
                    }
                    await wave.Value.updatewaveAsync(graphClient, manageddevices.Count());
                }
            }

            foreach (var wave in waves)
            {
                if (wave.Value.change == false)
                {
                    continue;
                }

                var difference = wave.Value.countgoal - wave.Value.count;
                if (difference > 0)
                {
                    //need to add to this wave
                    var availabledevices = GetUnassignedDevices(manageddevices, waves, difference);
                    foreach (var device in availabledevices)
                    {
                        try
                        {

                            var requestBody = new ReferenceCreate
                            {
                                OdataId = $"https://graph.microsoft.com/v1.0/directoryObjects/{device.Id}"
                            };

                            await graphClient.Groups[wave.Value.entraid].Members.Ref.PostAsync(requestBody);

                            difference--;
                        }
                        catch (Exception e)
                        {
                            log.LogError(e.ToString());
                        }
                    }
                    await wave.Value.updatewaveAsync(graphClient, manageddevices.Count());
                }
            }
        return "ok";
        }

        public static List<Microsoft.Graph.Models.Device> GetUnassignedDevices(List<Microsoft.Graph.Models.Device> manageddevices, Dictionary<int, wave> waves, int count)
        {
            var unassigneddevices = new List<Microsoft.Graph.Models.Device>();
            int cycle = 0;
            foreach (var device in manageddevices)
            {
                var duplicate = false;
                foreach (var substwave in waves)
                {
                    foreach (var substdevice in substwave.Value.devices)
                    {
                        if (substdevice.Id == device.Id)
                        {
                            duplicate = true;
                        }
                    }
                }
                if (duplicate) { continue; }
                unassigneddevices.Add(device);
                cycle++;
                if (cycle == count) { break; }
            }
            return unassigneddevices;
        }
    }

    public class wave
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public float percentagegoal { get; set; }
        public int count { get; set; }
        public int countgoal { get; set; }
        public bool change { get; set; }
        public List<DirectoryObject?> devices { get; set; }
        public string entraid { get; set; }

        public async Task updatewaveAsync(GraphServiceClient graphClient, int totaldevices)
        {
            var wavedevices = await graphClient.Groups[entraid].Members.GetAsync();
            this.count = wavedevices.Value.Count();
            this.devices = wavedevices.Value;

            this.countgoal = (int)(totaldevices * this.percentagegoal);

            if (this.count != this.countgoal < 0)
            {
                this.change = true;
            }
            return;
        }
    }
}
Code language: JavaScript (javascript)

Latest Articles

SOAR: Block Log Analytics IP Entities on Azure Frontdoor / WAF #3
How it works Previously, I’ve blogged about two variants that we used at Prof-IT Services to block malicious IP addresses on Azure Frontdoor that were ...
The G-Door: Microsoft 365 & the risk of unmanaged Google Doc accounts
It’s time to secure Google Workspace—even if you’re not using it. Read about our recent discovered vulnerability, called 'G-Door', which allows users to bypass Microsoft ...
Automating Azure SQL Maintenance with Azure Automation
Keeping Your Azure SQL Databases Healthy: The Power of Automation In the realm of database management, maintaining optimal performance and storage efficiency for your Azure ...
Malware Analysis – Shortcuts in zip file
Recently, we encountered two distinct variants of a payload delivered through Google Drive, both containing a malicious shortcut. While these threats were successfully mitigated, it’s ...