Windows 10 – New rules, new possibilities

Tonight – Microsoft dropped a bomb, again.

In Febuary this year, the day before I was going onto the stage at the Nordic Infrastructure Conference (NIC) in Oslo, Microsoft dropped the first one when the announced the changes to release cadence, support and lifecycle of Windows 10 and Office 365 ProPlus. That evening, I worked hard to update my session on how to manage Windows as a Service to suit the new rules.

Next week, I’ll be presenting the same session again, at the Cloud Infrastruture User Group in London and at the TechUG in Manchester, and again, ill be working hard to once more keep it up to date with new rules.

Just a few hours ago, Jared Spataro (Corporate Vice President for Office and Windows Marketing) published a blogpost that included three announcements: Two great once, which I’ve been keeping my eyes on for a while, that ill explain briefly and get back to in upcoming sessions and blog posts. The third one, to be completely honest, gives me very mixed feelings – a mixture of disappointment, frustration, admiration and hope.

Lets start with the first two:

Desktop Analytics,

Just the last week I delivered a session on Windows Analytics in Glasgow – a session that Ill be delivering in an extended format during Techdays in Sweden in a couple of months. Windows Analytics consist of Upgrade Readiness, Update Compliance and Device Health – and to me, its a vital part of any organizations desktop management infrastructure. It gives you great insight, enables you to make informed decisions and follow up on compliance.

However, its been missing a few pieces – and especially lacked the ability to make the hard choices for us. Now, its being transformed into Desktop Analytics – that promise to enable organizations to drive the adoption and servicing of Windows 10 and Office 365 ProPlus in a completely new way. Using telemetry data and machine learning – it will not just show which apps and drivers that will work on Windows 10, it will even build the most optimal pilot groups for you, based on your unique environment.

This will ensure a great user experience and a efficient, but still high quality, reactive testing. More info on this will be released during Ignite – and ill of course be blogging and speaking about it in a close future.

20180830_135841

Desktop App Assure,

Desktop App Assure will soon be yet another service provided by Microsoft FastTrack. This service, that will be offered to customers on Windows 10 Enterprise and Education for free, will ensure that the migration to Windows 10 (or between Windows 10 releases) is a smooth one. Delivering on the promise of app compatibility, customers that struggle with incompatible apps, will get help from FastTrack – again free of charge – to solve the incompatibilities and make the app run on the selected release of Windows 10. This for sure is an welcome addition and something that will put Microsoft closer to their customers – and probably also ensure that the data displayed in Desktop Analytics will be even more accurate. The service will roll out first of all (in preview) in North America October 1st (so just after Ignite) and reach world wide availability in February 2019. Again, more info will be released during Ignite, so ensure to follow me and #KneeDeepinTech on Twitter and also keep an eye out for new podcasts episodes!

Servicing and support flexibility,

Lastly, the biggest and most important announcement – which probably will fill my mind with thoughts for weeks.

I’ve always argued that ANY and EVERY organization out there could adopt Windows as a Service. I’ve never said its easy or without challenges, but no matter the complexity or size I’m still absolutely convinced about it. I’ve had discussions with probably hundreds of organizations over the years since the release of Windows 10. Many of these have been, noisy, to say the least. I’ve read thousands of tweets, posts and blogs around how crazy (and almost evil) Microsoft were when they introduced WaaS. I’ve offered to help, but few have been willing to change. I’ve been proud in supporting the thought and vision of Windows as a Service and the modern desktop.

Just three days ago I had a great meeting with a customer that have changed, and seen all the benefits of it, and they wanted to take the next step. In that meeting, I said that I didn’t believe that Microsoft would change the release cadence or the life cycle of Windows 10 releases. The reason for that? They have success stories, they have (in large part thanks to partners and the community) all the tools necessary to make it work. They have a number of partners & evangelist that been giving the process great thought and ensured a safe, user-friendly and cost efficient servicing model, and they have been defending their vision – in the interest of their customers, IT-security and innovation.

Today, it all changed, again. Ill briefly explain the changes, ill briefly explain the options – and ill definitely get back with more details in upcoming post, talks, webinars and podcasts.

First – there will still be two releases of Windows (and Office ProPlus) every year targeted for Mars and September. However, these two releases will have different length of their lifecycle. All the Mars releases will stay on the current support length of 18 months. The September release will (mind ONLY for Enterprise and Education) have an extended support of 30 months.

Releae_Info

Second – all currently released (and supported) versions of Windows 10 (1607, 1703, 1709 and 1803) will get extended support (again ONLY Enterprise and Education) for 30 months in totalt. This will extend the time organizations that already deployed their first version of Windows 10 get to roll out their next feature release of Windows. You can read all about the new end-of-support dates here.

Third – Windows 7 gets new options for extended security updates. If you would like to receive security updates after the official end of support date (January 14 2020) you can pay a per device fee for up to three more years, until January 2023. The price is yet to be announced, but the price per device will increase for each year until January 2023. This offering will be available to any organization with volume licensing for Windows 7 Pro or Enterprise. If you have a Software Assurance agreement, or already own licenses for Windows 10 Enterprise or Education, you’ll get a discounted price.

In addition to this, if you buy the Extended Security Updates (ESU) – you’ll be able to keep on running Office 365 ProPlus on Windows 7 for the duration of your ESU.

Forth – Office 365 ProPlus will be supported on Windows 8.1 until January 2023. Offie 365 ProPlus will be supported on Windows Server 2016 until October 2025.

And Fifth – If you run Office 2016 today and connect to Office Online Service such as Exchange – you’ve received three more years to migrate to the Office 365 ProPlus or Office 2019. The previous statement that Office 2016 would be unable to connect to online services from October 2020 has been change, and 2016 will be able to keep connecting to Exchange online (and other services) until October 2023.

Why is this important and how could this affect my organization?

street-381227_1280

Basically, Microsoft has listened to their customers and moved back a bit on their vision on moving organizations to a more modern desktop platform. They are still enabling organizations to drive modernization, to stay current, secure and modern. They are now allowing organizations that needed more time to change to do so – to modernize their applications, to change mindset and get current.

Its important, cause it shows that Microsoft listens and are willing to change – and its important because organizations are now being given a choice and a opportunity.

First – you now get more choices when it comes to choosing your Windows Servicing cadence. You can choose to go with only the September release, only the Mars release, both or choose one or both depending on the included features. This is good, and enables organizations that (due to the nature of their work or vertical) had a challenge with the months of release. Ill dig deeper into this topic in upcoming posts.

Second – You get to choose how fast you would like to make your organization more modern. This is something every organization need to consider, choose and plan for. I still recommend that you aim for a fairly quick servicing model, where you enable a more modern workplace and desktop. If you choose to take it a bit slower, ensure that you stay on route and stick to a plan.

What I’m afraid of, is that we in a couple of years time will end up in the exact same discussions again. People do not like change, and (and I honestly hope that I’m wrong here) I believe that some organizations wont change. They may migrate to Windows 10, and yes, they are still forced to upgrade more frequently than before, but there’s not the same need to actually change how they work with IT. There’s not the same need to modernize applications, and they will probably end up with a less secure environment than they would if they kept the, now old, pace of Windows as a Service.

Last words,

There are a huge amount of other opportunities and challenge that needs to be reviewed and discussed following these changes. Ill probably write about a number of them in the following weeks – and we’ll learn plenty more during Ignite.

But – takes this for what it is. This is something many have been asking for, now when they get it, I sure hope they take the opportunity to actually change. To make the most of it – to be thankful for the opportunity and deliver on the promise that many have made: “If we only get a bit more time, we will be able to change”.

So, now you have time. Make the most of it. Get Modern.

 

Tagged with: , , , , , , , , , , , , , , , , , , , , ,
Posted in Intune, Microsoft, News, Office 365, WaaS Toolbox, Windows 10, Windows as a Service, Windows Insider

Version 0.5 of the third Intune tool for Window 10 in the WaaS Toolbox

So, it’s time. It’s with mixed feeling that I write (and publish) this post, but I feel that it’s necessary for me to let you have a look and gather your feedback. So, please, feel free to reach out to me with your comments, thoughts and advices. I’m publishing the code and my thoughts in a 0.5 version. I’m still waiting for some feedback from friends and colleagues, its not complete yet but I do think that you’ll get the point of the script and the logic behind it. Further down ill have a short to-do list with all the things I would like to include before considering this 1.0. However, feel free to try it (I do not recommend to try it in production – and if you do, its at your own risk).

So, here we go. The third tool is a tool that you run before starting your test-phase and compatibility-phase of every new Windows 10 release. The main point of it is to create a “randomized” (more on that later) group that could change between every release – even though many devices and persons probably still would be included every time. So why do this with a script?

  • The randomization, which some my not agree on, creates a better test group than if you would have the same group over and over again, or manually create one every time.
  • It saves time – obviously – to create test groups is (in my experience) time consuming.
  • It will keep the users happy – no one wants to be the poor user who always gets to try new things first.
  • It eases the process of Windows Servicing in your organization – just as with anything when it comes to automation, or processes in general, the more things/parts you can create a standardized step for – the better.
  • Lastly – its not bias. It cloud include the CEO, it could include that user that always gives you a hard time, it could exclude people that you never hear back from (and if you don’t hear anything its all good, right?)

So, currently the script consist of three parts:

  1. A bunch of functions from the Intune product group (available here) and Nickolaj Andersen, but also, something of (kind of) my own. I’ve modified one of the existing Powershell functions (Add-ApplicationAssignment) to be more inclusive and handle other assignments rather than group assignments. But it will also remove any group assignments where the Azure AD group has been deleted. I’ve split this into two functions/cmdlets of my own, where one is a part of this script (the other one is saved for a later blogpost). The included one will loop through all apps, prior to doing the selection of devices for the compatibility group, and remove “orphaned” assignments – while keeping all others (including exclusion). I’ve given it the rather boring name: “Remove-EmptyApplicationAssignments”. Ill, as with the other cmdlet, dig deeper into these in a follow up post.

    Born_To_Die_Group

    The function can be found below. I’ve done tweaks and added some parts, but the real work has been done by the Intune team.

    </span>
    
    Function Remove-EmptyApplicationAssignments(){
    
    
    
    [cmdletbinding()]
    
    $graphApiVersion = "Beta"
    
    try {
    
    $AllApps = (Get-IntuneApplication).id
    
    foreach ($ApplicationId in $AllApps) {
    
    $AssignedGroups = (Get-ApplicationAssignment -ApplicationId $ApplicationId).assignments
    
    if($AssignedGroups){
    
    $App_Count = @($AssignedGroups).count
    $i = 1
    
    # Creating header of JSON File
    $JSON = @"
    
    {
    "mobileAppAssignments": [
    
    "@
    
    # Looping through all existing assignments and adding them to the JSON object
    foreach($Assignment in $AssignedGroups){
    
    $ExistingTargetGroupId = $Assignment.target.GroupId
    $ExistingInstallIntent = $Assignment.intent
    # Finding out if the assignment is targeted to All User or All Devices and adding it to JSON object
    
    if(!$ExistingTargetGroupId){
    
    if ($Assignment.target.'@odata.type' -match '#microsoft.graph.allLicensedUsersAssignmentTarget') {
    
    $JSON += @"
    
    {
    "@odata.type": "#microsoft.graph.mobileAppAssignment",
    "target": {
    "@odata.type": "#microsoft.graph.allLicensedUsersAssignmentTarget"
    },
    "intent": "$ExistingInstallIntent"
    "@
    
    }
    
    elseif ($Assignment.target.'@odata.type' -match '#microsoft.graph.allDevicesAssignmentTarget') {
    
    
    $JSON += @"
    
    {
    "@odata.type": "#microsoft.graph.mobileAppAssignment",
    "target": {
    "@odata.type": "#microsoft.graph.allDevicesAssignmentTarget"
    },
    "intent": "$ExistingInstallIntent"
    "@
    
    
    }
    
    }
    
    # Testing the Azure AD group object ID to see if the group exist in Azure AD. If not, the foreach loop will exit without adding the assignment to JSON. Note, this will throw an error message created from Get-AADGroup, but will continue
    
    else{
    
    try {
    
    Get-AADGroup -id $ExistingTargetGroupId -ErrorAction SilentlyContinue | Out-Null
    
    }
    
    catch {
    
    'This group does not exist in Azure AD'
    }
    
    # Finding out if the assignment is an exclusion of an Azure AD Group and adding the assignment to JSON
    
    if($Assignment.target.'@odata.type' -match '#microsoft.graph.exclusionGroupAssignmentTarget'){
    
    $JSON += @"
    
    {
    "@odata.type": "#microsoft.graph.mobileAppAssignment",
    "target": {
    "@odata.type": "#microsoft.graph.exclusionGroupAssignmentTarget",
    "groupId": "$ExistingTargetGroupId"
    },
    "intent": "$ExistingInstallIntent"
    "@
    
    }
    
    # Adding the group assignment to JSON
    
    else{
    
    $JSON += @"
    
    {
    "@odata.type": "#microsoft.graph.mobileAppAssignment",
    "target": {
    "@odata.type": "#microsoft.graph.groupAssignmentTarget",
    "groupId": "$ExistingTargetGroupId"
    },
    "intent": "$ExistingInstallIntent"
    "@
    
    }
    
    }
    
    if($i -ne $App_Count){
    
    $JSON += @"
    
    },
    
    "@
    
    }
    
    else {
    
    $JSON += @"
    
    }
    
    "@
    
    }
    
    $i++
    
    }
    
    # Adding close of JSON object
    $JSON += @"
    
    ]
    }
    
    "@
    
    $Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign"
    $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
    Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json"
    
    }
    
    }
    
    }
    
    # This will throw an error message if try fails - but not if the message is $null
    
    catch {
    
    $ex = $_.Exception
    if($ex) {
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break
    
    }
    
    }
    
    }
    
    <span style="display:inline !important;float:none;background-color:transparent;color:#3d596d;cursor:text;font-family:'Noto Serif', Georgia, 'Times New Roman', Times, serif;font-size:16px;font-style:normal;font-variant:normal;font-weight:400;letter-spacing:normal;line-height:19.2px;orphans:2;text-align:left;text-decoration:none;text-indent:0;text-transform:none;white-space:normal;word-spacing:0;">

  2. The second part is the first part where things actually happens. The first part of the script focuses on hardware, and will use inventory data from Intune to get all existing hardware models. It will then choose one unique device and add that, its user and all of the users Windows devices to the application compatibility group. If you would like to know more about these groups, please read my blogpost on “The first tool in the toolbox”.
  3. The third part is probably the most complex one. This one looks at all applications that can be targeted to Windows devices (Apps, MSI & Office ProPlus deployments) and fetches all devices and users in each of the assigned groups. (Note! One of the things on the roadmap is to handle assignments of apps only to All Devices/All Users – which currently is ignored) It will then compare the already added users/devices in the compatibility group and thereafter choose two objects (users and/or devices) to add. It will then follow the same principle as with models. If a device is added, its user and that users other Windows devices will be added as well. If a user is added, all of that users Windows devices are added.

This is where the script ends currently. Ill first answer a question that I believe you are asking yourself – why all theses machines/users? There are many reasons for that, but my thinking goes like this:

Overall, the compatibility group should be about 10-15 % of your organization. We should aim to cover all: Models, Applications, Locations, Networks, Policies/OUs, which probably gets us to 10-15 % or close to it.

The reason for including all of a users devices is to ensure that we actually get a device with the application tested. Some devices may be old, some may not be used for a specific purpose etc. Also, this ensure a consistent user experience for the user. In the same way as my second tool (Self-Service deferral) deferred all devices when a user were added to the group, the same inclusion applies here.

Its also to do with reporting, ease of information, management and reporting. As Intune, by its own and currently (Power BI can help you a lot here) aren’t as extensive reporting-wise as ConfigMgr – I’ve choose to create groups that you would be able to report on and review easier. I’m however happy to get your feedback on this design decision.

I can keep going, but as this is a 0.5 post – ill leave you here with the to-do list and the script. I’m happy for all kinds of feedback and hope you either can use the script, or get inspiration from it and create something great on your own. The best thing about this particular script so far is that I get to learn a lot about Graph, GitHub and JSON – which will come in handy moving forward as well.

And the last note! Review the script closely – some variables are hardcoded atm and some are fetched from Azure Automation. As this is a work in progress, you may need to do some tweaks, add variables etc to make it work. The script will also throw an error message generated from the Get-AADGroup cmdlet when it finds a group that doesn’t exist. This is fine, but doesn’t look to good. I’m happy to assist if needed! Just reach out to me on Twitter or via comments.

AGAIN – DO NOT run this in production if you aren’t absolutely sure about what you are doing. I’m happy to help explaining, but I wont take any responsibility if it breaks something.

To-Do (before 1.0):

  • Complete the script with other variables (as much as possible at least): Location, Network, policies etc.
  • Support for all users/all devices in the app section.
  • Clean it up! Variables, indentation, work notes etc
  • Document it!
  • Extended error handling.

The code is of course also available on GitHub!

</span>

#Author: Simon Binder
#Blog: bindertech.se
#Twitter: @Bindertech
#Thanks to: @daltondhcp, @davefalkus, @NickolajA and the Intune product group.
#Most functions is copied from the Powershell Intune Samples: https://github.com/microsoftgraph/powershell-intune-samples
#Script requires the Azure AD Powershell module or the Azure AD Preview Powershell module to run

# Import required modules
try {
Import-Module -Name AzureAD -ErrorAction Stop
Import-Module -Name PSIntuneAuth -ErrorAction Stop
}
catch {
Write-Warning -Message "Failed to import modules"
}

function Get-AuthToken {



[cmdletbinding()]

param
(
[Parameter(Mandatory=$true)]
$User
)

$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User

$tenant = $userUpn.Host

Write-Host "Checking for AzureAD module..."

$AadModule = Get-Module -Name "AzureAD" -ListAvailable

if ($AadModule -eq $null) {

Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview"
$AadModule = Get-Module -Name "AzureADPreview" -ListAvailable

}

if ($AadModule -eq $null) {
write-host
write-host "AzureAD Powershell module not installed..." -f Red
write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow
write-host "Script can't continue..." -f Red
write-host
exit
}

# Getting path to ActiveDirectory Assemblies
# If the module count is greater than 1 find the latest version

if($AadModule.count -gt 1){

$Latest_Version = ($AadModule | select version | Sort-Object)[-1]

$aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version }

# Checking if there are multiple versions of the same module found

if($AadModule.count -gt 1){

$aadModule = $AadModule | select -Unique

}

$adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
$adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"

}

else {

$adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
$adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"

}

[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null

[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null

$clientId = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547"

$redirectUri = "urn:ietf:wg:oauth:2.0:oob"

$resourceAppIdURI = "https://graph.microsoft.com"

$authority = "https://login.microsoftonline.com/$Tenant"

try {

$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority

# https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx
# Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession

$platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto"

$userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId")

$authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result

# If the accesstoken is valid then create the authentication header

if($authResult.AccessToken){

# Creating header for Authorization token

$authHeader = @{
'Content-Type'='application/json'
'Authorization'="Bearer " + $authResult.AccessToken
'ExpiresOn'=$authResult.ExpiresOn
}

return $authHeader

}

else {

Write-Host
Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red
Write-Host
break

}

}

catch {

write-host $_.Exception.Message -f Red
write-host $_.Exception.ItemName -f Red
write-host
break

}

}

####################################################

Function Get-AADDevice(){



[cmdletbinding()]

param
(
$DeviceID
)

# Defining Variables
$graphApiVersion = "v1.0"
$Resource = "devices"

try {

$uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=deviceId eq '$DeviceID'"

(Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value

}

catch {

$ex = $_.Exception
$errorResponse = $ex.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$reader.BaseStream.Position = 0
$reader.DiscardBufferedData()
$responseBody = $reader.ReadToEnd();
Write-Host "Response content:`n$responseBody" -f Red
Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
write-host
break

}

}

####################################################

Function Get-ManagedDevices(){



[cmdletbinding()]

param
(
[switch]$IncludeEAS,
[switch]$ExcludeMDM
)

# Defining Variables
$graphApiVersion = "beta"
$Resource = "deviceManagement/managedDevices"

try {

$Count_Params = 0

if($IncludeEAS.IsPresent){ $Count_Params++ }
if($ExcludeMDM.IsPresent){ $Count_Params++ }

if($Count_Params -gt 1){

write-warning "Multiple parameters set, specify a single parameter -IncludeEAS, -ExcludeMDM or no parameter against the function"
Write-Host
break

}

elseif($IncludeEAS){

$uri = "https://graph.microsoft.com/$graphApiVersion/$Resource"

}

elseif($ExcludeMDM){

$uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'"

}

else {

$uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'"
Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices"
Write-Host

}

(Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

}

catch {

$ex = $_.Exception
$errorResponse = $ex.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$reader.BaseStream.Position = 0
$reader.DiscardBufferedData()
$responseBody = $reader.ReadToEnd();
Write-Host "Response content:`n$responseBody" -f Red
Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
write-host
break

}

}

####################################################

Function Get-ManagedDeviceUser(){



[cmdletbinding()]

param
(
[Parameter(Mandatory=$true,HelpMessage="DeviceID (guid) for the device on must be specified:")]
$DeviceID
)

# Defining Variables
$graphApiVersion = "beta"
$Resource = "deviceManagement/manageddevices('$DeviceID')?`$select=userId"

try {

$uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
Write-Verbose $uri
(Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).userId

}

catch {

$ex = $_.Exception
$errorResponse = $ex.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$reader.BaseStream.Position = 0
$reader.DiscardBufferedData()
$responseBody = $reader.ReadToEnd();
Write-Host "Response content:`n$responseBody" -f Red
Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
write-host
break

}

}

####################################################

Function Get-IntuneApplication(){



[cmdletbinding()]

$graphApiVersion = "Beta"
$Resource = "deviceAppManagement/mobileApps"

try {

$uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
(Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { (!($_.'@odata.type').Contains("managed")) }

}

catch {

$ex = $_.Exception
Write-Host "Request to $Uri failed with HTTP Status $([int]$ex.Response.StatusCode) $($ex.Response.StatusDescription)" -f Red
$errorResponse = $ex.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$reader.BaseStream.Position = 0
$reader.DiscardBufferedData()
$responseBody = $reader.ReadToEnd();
Write-Host "Response content:`n$responseBody" -f Red
Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
write-host
break

}

}

####################################################

Function Get-ApplicationAssignment(){



[cmdletbinding()]

param
(
$ApplicationId
)

$graphApiVersion = "Beta"
$Resource = "deviceAppManagement/mobileApps/$ApplicationId/?`$expand=categories,assignments"

try {

if(!$ApplicationId){

write-host "No Application Id specified, specify a valid Application Id" -f Red
break

}

else {

$uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
(Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get)

}

}

catch {

$ex = $_.Exception
$errorResponse = $ex.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$reader.BaseStream.Position = 0
$reader.DiscardBufferedData()
$responseBody = $reader.ReadToEnd();
Write-Host "Response content:`n$responseBody" -f Red
Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
write-host
break

}

}

####################################################

Function Remove-EmptyApplicationAssignments(){



[cmdletbinding()]

$graphApiVersion = "Beta"

try {

$AllApps = (Get-IntuneApplication).id

foreach ($ApplicationId in $AllApps) {

$AssignedGroups = (Get-ApplicationAssignment -ApplicationId $ApplicationId).assignments

if($AssignedGroups){

$App_Count = @($AssignedGroups).count
$i = 1

# Creating header of JSON File
$JSON = @"

{
"mobileAppAssignments": [

"@

# Looping through all existing assignments and adding them to the JSON object
foreach($Assignment in $AssignedGroups){

$ExistingTargetGroupId = $Assignment.target.GroupId
$ExistingInstallIntent = $Assignment.intent
# Finding out if the assignment is targeted to All User or All Devices and adding it to JSON object

if(!$ExistingTargetGroupId){

if ($Assignment.target.'@odata.type' -match '#microsoft.graph.allLicensedUsersAssignmentTarget') {

$JSON += @"

{
"@odata.type": "#microsoft.graph.mobileAppAssignment",
"target": {
"@odata.type": "#microsoft.graph.allLicensedUsersAssignmentTarget"
},
"intent": "$ExistingInstallIntent"
"@

}

elseif ($Assignment.target.'@odata.type' -match '#microsoft.graph.allDevicesAssignmentTarget') {


$JSON += @"

{
"@odata.type": "#microsoft.graph.mobileAppAssignment",
"target": {
"@odata.type": "#microsoft.graph.allDevicesAssignmentTarget"
},
"intent": "$ExistingInstallIntent"
"@


}

}

# Testing the Azure AD group object ID to see if the group exist in Azure AD. If not, the foreach loop will exit without adding the assignment to JSON. Note, this will throw an error message created from Get-AADGroup, but will continue

else{

try {

Get-AADGroup -id $ExistingTargetGroupId -ErrorAction SilentlyContinue | Out-Null

}

catch {

'This group does not exist in Azure AD'
}

# Finding out if the assignment is an exclusion of an Azure AD Group and adding the assignment to JSON

if($Assignment.target.'@odata.type' -match '#microsoft.graph.exclusionGroupAssignmentTarget'){

$JSON += @"

{
"@odata.type": "#microsoft.graph.mobileAppAssignment",
"target": {
"@odata.type": "#microsoft.graph.exclusionGroupAssignmentTarget",
"groupId": "$ExistingTargetGroupId"
},
"intent": "$ExistingInstallIntent"
"@

}

# Adding the group assignment to JSON

else{

$JSON += @"

{
"@odata.type": "#microsoft.graph.mobileAppAssignment",
"target": {
"@odata.type": "#microsoft.graph.groupAssignmentTarget",
"groupId": "$ExistingTargetGroupId"
},
"intent": "$ExistingInstallIntent"
"@

}

}

if($i -ne $App_Count){

$JSON += @"

},

"@

}

else {

$JSON += @"

}

"@

}

$i++

}

# Adding close of JSON object
$JSON += @"

]
}

"@

$Resource = "deviceAppManagement/mobileApps/$ApplicationId/assign"
$uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json"

}

}

}

# This will throw an error message if try fails - but not if the message is $null

catch {

$ex = $_.Exception
if($ex) {
$errorResponse = $ex.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$reader.BaseStream.Position = 0
$reader.DiscardBufferedData()
$responseBody = $reader.ReadToEnd();
Write-Host "Response content:`n$responseBody" -f Red
Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
write-host
break

}

}

}
####################################################

Function Get-AADGroup(){



[cmdletbinding()]

param
(
$GroupName,
$id,
[switch]$Members
)

# Defining Variables
$graphApiVersion = "v1.0"
$Group_resource = "groups"

try {

if($id){

$uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=id eq '$id'"
(Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

}

elseif($GroupName -eq "" -or $GroupName -eq $null){

$uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)"
(Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

}

else {

if(!$Members){

$uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'"
(Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

}

elseif($Members){

$uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)?`$filter=displayname eq '$GroupName'"
$Group = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

if($Group){

$GID = $Group.id

$Group.displayName
write-host

$uri = "https://graph.microsoft.com/$graphApiVersion/$($Group_resource)/$GID/Members"
(Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

}

}

}

}

catch {

$ex = $_.Exception
$errorResponse = $ex.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$reader.BaseStream.Position = 0
$reader.DiscardBufferedData()
$responseBody = $reader.ReadToEnd();
Write-Host "Response content:`n$responseBody" -f Red
Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
write-host
break

}

}

####################################################

# Read credentials and variables

$AADCredential = Get-AutomationPSCredential -Name "WaaSAccount"
$Credential = Get-AutomationPSCredential -Name "WaaSAccount"
$AppClientID = Get-AutomationVariable -Name "AppClientID"
$WindowsVersion = Get-AutomationVariable -Name "WindowsVersion"
$Tenantname = Get-AutomationVariable -Name "Tenantname"
$DefaultGroup = Get-AutomationVariable -Name "DefaultGroup"
$Groups = New-object Microsoft.Open.AzureAD.Model.GroupIdsForMembershipCheck
$TargetOSVersion = Get-AutomationVariable -name "TargetOSVersion"
#Replace with Automation Variables before production
$TechPilot = '758ba924-7961-4453-a3bf-da6040cea6d3'
$ApplicationPilot = '3141ba35-8f3d-41df-bd5c-4633c971af65'
$DeploymentRings = '836c8ef0-a604-4d5d-8d9b-ca92fedcd573','8f7b9464-4293-4929-92cd-a7cb28eeb8c3'
#Required for now, its the Windows 10 buildnumber without dots
$TargetOSVersion = 10017134165
# Acquire authentication token
try {
Write-Output -InputObject "Attempting to retrieve authentication token"
$AuthToken = Get-MSIntuneAuthToken -TenantName $Tenantname -ClientID $AppClientID -Credential $Credential
if ($AuthToken -ne $null) {
Write-Output -InputObject "Successfully retrieved authentication token"
}
}
catch {
Write-Warning -Message "Failed to retrieve authentication token"
}

Connect-AzureAD -Credential $AADCredential

#Getting all Windows Devices from Intune, removing already upgraded machines.

$AllWindowsDevices = Get-ManagedDevices | Where-Object -Property 'OperatingSystem' -EQ 'Windows'
$AllOldWindowsDevices = New-Object 'System.Collections.Generic.List[System.Object]'
foreach ($WindowsDevice in $AllWindowsDevices) {

$NewVersion = $WindowsDevice | Select-Object -ExpandProperty osVersion
$Newversion = $NewVersion -replace '\.',''
$WindowsDevice.osVersion="$NewVersion"

if ($WindowsDevice.osVersion -lt $TargetOSVersion) {

$AllOldWindowsDevices.Add($WindowsDevice)

}

}

#Populating the application pilot group.

#Getting all unique models, selecting machines at random. Finding the Intune-device owner and adding it and all of its devices to the Application Pilot group.

$AllModels = $AlloldWindowsDevices | Sort-Object -Property model -Unique | Select-Object Devicename, model, id, osVersion

foreach ($Model in $AllModels) {

$OwnerID = Get-ManagedDeviceUser -DeviceID $Model.id
$Owner = Get-AzureADUser -ObjectID $OwnerID
#Ensure to add device and user to group
try {
Add-AzureADGroupMember -ObjectId $ApplicationPilot -RefObjectId $Owner.ObjectId
}
catch {$DisplayName = $Owner.DisplayName

"$Displayname is already a member of this group"}

$MemberDevices = Get-AzureADUserOwnedDevice -ObjectId $OwnerID | Where-Object {$_.DeviceOSType -eq 'Windows'} | Select-Object -ExpandProperty ObjectID

foreach ($MemberDevice in $MemberDevices) {

try {
Add-AzureADGroupMember -ObjectId $Groups -RefObjectId $MemberDevice
}

catch {
"$MemberDevice is already a member of $Groups"

}

}

}
#Cleaning up all orphaned assignments prior to getting application groups.

Remove-EmptyApplicationAssignments

#Getting all Windows apps, LOB and O365 deployments, selecting machines at random. Finding the Intune-device owner and adding it and all of its devices to the Application Pilot group.

$Groups = $ApplicationPilot
$AllApps = Get-IntuneApplication
$AllWindowsApps = New-Object 'System.Collections.Generic.List[System.Object]'
foreach ($App in $AllApps) {

$StoreApp = $App | Where-Object -Property '@odata.type' -Match '#microsoft.graph.microsoftStoreForBusinessApp'

if ($StoreApp -ne $null) {

$AllWindowsApps.Add($StoreApp)

}

$MSI = $App | Where-Object -Property '@odata.type' -Match '#microsoft.graph.windowsMobileMSI'

if ($MSI -ne $null) {

$AllWindowsApps.Add($MSI)

}
$Office = $App | Where-Object -Property '@odata.type' -Match '#microsoft.graph.officeSuiteApp'

if ($Office -ne $null) {

$AllWindowsApps.Add($Office)

}

$Appx = $App | Where-Object -Property '@odata.type' -Match '#microsoft.graph.officeSuiteApp'

if ($Appx -ne $null) {

$AllWindowsApps.Add($Appx)

}

}

foreach ($WindowsApp in $AllWindowsApps) {

$Group = Get-ApplicationAssignment -ApplicationId $WindowsApp.id | Select-Object -ExpandProperty 'assignments' | Select-Object -ExpandProperty 'target'

if ($Group.'@odata.type' -eq $null) {"$Group This is not assigned to any group"}

elseif ($Group.'@odata.type' -match '#microsoft.graph.allLicensedUsersAssignmentTarget') {'This is assigned to all Users'}

elseif ($Group.'@odata.type' -match '#microsoft.graph.allDevicesAssignmentTarget') {'This is assigned to all Devices'}

elseif ($Group.'@odata.type' -match '#microsoft.graph.groupAssignmentTarget') {

try {
$AppGroupMembers = Get-AzureADGroupMember -ObjectId $group.groupId -All:$True | Group-Object 'ObjectType' -AsHashTable -AsString
}
Catch {"The group with ID $Group.groupid does no longer exist"}

if ($AppGroupMembers.user -ne $null) {

$AppGroupUserMembers = $AppGroupMembers
$CurrentMembers = Get-AzureADGroupMember -ObjectId $Groups -All:$True | Group-Object 'ObjectType' -AsHashTable -AsString

if ($CurrentMembers -ne $null) {

$Compared = Compare-Object -ReferenceObject $AppGroupUserMembers.user.objectid -DifferenceObject $CurrentMembers.user.objectid -IncludeEqual | Where-Object -Property 'SideIndicator' -Match '==' | Select-Object -ExpandProperty 'InputObject'

foreach ($Object in $Compared) {

if ($AppGroupUserMembers.user.objectid -contains $Object) {$AppGroupUserMembers = $AppGroupUserMembers.User.ObjectID -ne $Compared

}

$RandomMembers = $AppGroupUserMembers | Random -Count 2

foreach ($Member in $RandomMembers) {

$MemberDevices = Get-AzureADUserOwnedDevice -ObjectId $Member | Where-Object {$_.DeviceOSType -eq 'Windows'} | Select-Object -ExpandProperty ObjectID

foreach ($MemberDevice in $MemberDevices) {

try {

Add-AzureADGroupMember -ObjectId $Groups -RefObjectId $MemberDevice

}

catch {

"$MemberDevice is already a member of $Groups"

}
}

try {

Add-AzureADGroupMember -ObjectId $Groups -RefObjectId $Member

}

catch {

"$MemberDevice is already a member of $Groups"

}

}
}
}

}
if ($AppGroupMembers.Device -ne $null) {

$AppGroupDeviceMembers = $AppGroupMembers
$CurrentMembers = Get-AzureADGroupMember -ObjectId $Groups -All:$True | Group-Object 'ObjectType' -AsHashTable -AsString

if ($CurrentMembers -ne $null) {

$Compared = Compare-Object -ReferenceObject $AppGroupDeviceMembers.Device.objectid -DifferenceObject $CurrentMembers.Device.objectid -IncludeEqual | Where-Object -Property 'SideIndicator' -Match '==' | Select-Object -ExpandProperty 'InputObject'

foreach ($Object in $Compared) {

if ($AppGroupDeviceMembers.user.objectid -contains $Object) {$AppGroupDeviceMembers = $AppGroupDeviceMembers.User.ObjectID -ne $Compared

}
$RandomMembers = $AppGroupDeviceMembers | Random -Count 2

foreach ($Member in $RandomMembers) {

$MemberUsers = Get-ManagedDeviceUser -DeviceID $AppGroupDeviceMembers.Device.DeviceID
#May need to add code to handle multiple owners

foreach ($MemberUser in $MemberUsers) {

try {

Add-AzureADGroupMember -ObjectId $Groups -RefObjectId $MemberUser
}

catch {

"$MemberUser is already a member of $Groups"

}

}

try {

Add-AzureADGroupMember -ObjectId $Groups -RefObjectId $Member

}

catch {

"$Member is already a member of $Groups"

}

}
}
}

}

}

}

&nbsp;

<span style="display:inline !important;float:none;background-color:transparent;color:#3d596d;cursor:text;font-family:'Noto Serif', Georgia, 'Times New Roman', Times, serif;font-size:16px;font-style:normal;font-variant:normal;font-weight:400;letter-spacing:normal;line-height:19.2px;orphans:2;text-align:left;text-decoration:none;text-indent:0;text-transform:none;white-space:normal;word-spacing:0;">

 

 

Tagged with: , , , , , , , , , , , , ,
Posted in Intune, Microsoft, WaaS Toolbox, Windows 10, Windows as a Service

Sneak peak and request for feedback!

close up of text

Photo by Pixabay on Pexels.com

I’m currently working on the third tool in my “WaaS toolbox”-project. This tool will be used in the beginning of every new feature roll-out, but could be used for quality updates as well. The plan were to have at least a first release done this week, but due to the unpredictability of children, it will more likely be release next week.

The script will fetch information from Intune, again using Graph, and based on that create a compatibility test-group. The goal would be that this group should include users and devices that covers all possible configurations when it comes to hard, apps, profiles and network segments, and in total be around 10-15 % of your total estate.

I’m done with the first part which would be to gather all unique hardware models – and im currently finalizing the first draft for apps and applications. I wont share the code today, as i’m not 100 % happy with it and would need the entire overview done before I feel ready to share anything. However, to keep on working, I would like some feedback:

  • First, is this kind of tool something that you feel the need for?
    • What’s your view on the compatibility pilot, should it be recreated for each feature update or reused for everyone? If the latter is your preferred choice, should I even keep working on the script?
  • Second, what other variables would you be interested in – other than hardware, apps, networks and profiles?
  • Third, currently I’m adding a user and all of its Windows-devices to the pilot group. I feel that this would give the user more consistency. Would you rather see that I only added the user, or only one or a few of that users devices?

Also, any general feedback in this matter is most welcome! Please feel free to comment this post, ping me on Twitter (@Bindertech) or reach out via mail or LinkedIn.

Would you like to know more about the script or WaaS in general? Again, reach out to me! I’m more than happy to share any insights or even the incomplete script in that way.

Thank you for your feedback!

Tagged with: , , , , , , , , , , , , , ,
Posted in Azure, Intune, Microsoft, Windows 10, Windows as a Service

The second – and updated – tool in the WaaS Toolbox – Self-Service Deferral

New week and new tool! This is the remake of an older script that you can read about here (and also where you’ll get instructions on how to get started with Azure Automation – which ill rewrite when the toolbox is a bit more complete.

So, what does this one do? As I always say when talking about Windows as a Service, the hardest and most important challenge to solve is user experience. User rarely wants to get a restart in the middle of what they are doing – or even worse, a crashed PC or application in the middle of a project. Therefore, in my opinion, its vital that we give our users more choice when it comes to how and when to install a new feature upgrade (or quality update).

One of the approaches we can use is the ability for a user to Self-service defer them selves to a slower ring. This will put them at a later install date than they may have had when getting added to one of your deployment rings, and therefore giving them some more time to finalize a project or perhaps get back from a holiday without getting faced by a new operating system version.

What’s fun about this solution is that it leverages both the Graph API and the Microsoft Intune Powershell functions from the Intune team (found on GitHub), Microsoft Intune Software Update Rings, Azure Automation and Azure AD cmdlets – as well as self service group management (even if its not required) using Azure AD Premium. So you’ll put your EMS licenses to good use, with minimum effort.

So, first ill give you a brief overview about how the script works from a functionality point of view, and then ill explain some of the logic around the script.

Pre-reqs and functionality

This script assumes that you in some way have applied Software Update rings to some or all of your users or Windows 10 devices. This can be done partly using my other script that I blogged about last week: First tool in the toolbox – Create Software Update rings and groups in Intune

What’s missing is a script to actually populate the rings, but I’m working on that as we speak and will post it shortly. But you are of course free to populate your rings in the way you choose.

Next, I recommend that you setup Self-service group management in Azure AD – so that your users can add and remove themselves from the group. The script will of course work in the exact same way if you add or remove users manually or by script.

In my case, I’m using the property “FacsimileTelephoneNumber” in the User Azure AD objects to tattoo the group the user was a member of when he or she (and their devices) got deferred. If you are using this property for something else, it should be fine to replace that value with another one.

Lastly, you should configure a schedule for the script to run on from Azure Automation.

Optional: If the user isn’t a member of any Azure AD Group that are used for Windows Servicing, you could add a default group to put the user in when its removed from the deferral group. This is done using a variable in Azure Automation, not surprisingly name “DefaultGroup”. What you need to add to active this function is the GroupID for the group where you want to add your users. If you don’t enter this value, they’ll just get removed from the deferral group.

DefaultGroup.JPG

Next, a brief overview around what the script will do. This is the scenario its built for – (if you have other’s that you would like me to support, let me know!):

Azure AD Groups are populated with users and/or devices. These AAD groups are assigned to a number of different Windows 10 Software Update rings that applies settings for how feature- and quality upgrades and updates are applied. In my case I have two pilot rings, two deployment rings and two deferral groups, where one is used for Self-Service deferral.

When a user (or you as an administrator) would like to temporarily defer a user to a later install date the user object is added to the corresponding Azure AD Group. When the script runs, and you can choose your schedule based on the size of your environment and requirements,  it will look into the Self-Service deferral group and get all the user members from it.

The script will take note of the Azure AD Groups the users belongs to and add this info to the user object for later use.

It will then remove the user and all devices owned by that user from all other pilot- and deployment rings – and add the user and all its devices to the deferral group. This ensure that you don’t get challenges with more than one policy being applied to the user or its devices. The downside being that this today only supports deferral of the user and all its devices and not individual devices (if that’s a requirement).

When the user is OK with getting the newest upgrade or update, the object should be removed from the Self-Service deferral group and the next time the script runs – it will re-add the user and its devices to the group that were added to the user object.

The reason for all this is to keep the user and admin-experience as simple as possible. All you need to do is to add or remove user objects from a specific Azure AD Group, and its also enables self-service by either the included feature in Azure AD Premium or any other Self-Service tool that supports adding users to Azure AD Groups, or even synchronized AD groups, though I haven’t tested that scenario. Later on ill do another script that supports ConfigMgr and Hybrid scenarios (both Co-management and Hybrid AD Join) but that’s in the roadmap.

Now, lets get into the script:

Script and logic

You’ll find the script on GitHub as well as at the bottom of this post.

As with my first tool, this tools uses a number of Functions from the Intune team built on the Graph API – as well as Nickolaj Andersens Powershell module to get the authentication token. These are imported and added before starting the script that then authenticates to both Intune and Azure AD.

Script_Part1

We then set a number of variable from Azure Automation as well as create a new object that we later will use in a function to compare membership in groups. Lastly we’ll gather a number of variables from Azure AD to be able to find groups, users and devices. When all that’s done, the rest of the script is divided into three parts:

  • Gather information of users and their devices as well as enable re-enablement of deployment rings.
  • Deferral of users and devices.
  • Re-enablement of users and devices.

Script_Part2.JPG

In the first part of the script, we’ll start by checking of the user is a member of any of the pilot or deployment rings. If the user is a member, we’ll tattoo the name of that group in to the FacsimileTelephoneNumber property of that user, to be used later on. If the user is a member of more than one group (which they shouldn’t be, but stuff happends :)) the script will choose the first group that gets returned – and if the user aren’t a member of any group – the first half of the first part of the script wont be used. Lastly, the user itself will be removed from the pilot- or deployment groups of which they are members.

Next, we’ll get all devices that are owned by this user. I’m using the AzureAD cmdlet for this rather than the Intune one as I feel that I get more information from AzureAD than from Intune as this stage. However, the challenge is that the AzureAD cmdlet (or the Azure AD object probably) can have more than one user – and with the Intune function it only returns one. Later on, ill match the result to get ONE user, but that’s something to have in mind. I need to look into it later on.

All the (Windows) devices that have the user as owner will be added to a variable. They are then checked to see if they already are member of the deferral group, and if they are not, they’ll be added. That concludes the first part of the script.

Script_Part3.JPG

The second part of the script is the part that does the actual deferral – all the devices from all the deferred users are checked to see if they are member of any of the pilot- or deployment groups. If they are, they’ll be removed from these groups.

Script_Part4.JPG

The third and last part of the script (which is also the largest part) is all about restoring users and devices when they no longer need to be deferred. It will look at all the devices in the group and compare them to the deferred devices (based on the users of the group). So, to be restored, you first need to remove the user object from the group. The script will then compare the members to the variable of deferred devices and remove the devices that for any reason are deferred but don’t have there owner in the group.

Based of the owners value in FacsimileTelephoneNumber they’ll be either restored to that group or to a default group that you may set as a variable. If no default group has been set, the devices will just be removed. As for the user (or the Owner), the it or them will also be restored to the group based on the same conditions as the devices. In that way, you can have a user-centric approach to Windows Servicing – instead of looking at particular devices.

Script_Part5.JPG

There may be scenarios when you would like to defer individual machines. This is currently not supported by this script – but instead (when using my first tool that creates the groups and profiles) a second group and profile are created to manually (or later on integrated with Upgrade Readiness, but again – its a LATER story) add individual machines to.

I do consider the script functional, but there are still things to work on. Ill do my best to improve performance and error-handling, but I welcome of kinds of feedback. The next step will be to create a script that populates WaaS-groups based on the information provided by Intune – and later also (as stated above) a integration to WA and Upgrade Readiness.

Try it out (at your own risk of course :)) and let me know if you have any feedback or feature requests!


#Author: Simon Binder
#Blog: bindertech.se
#Twitter: @Bindertech
#Thanks to: @daltondhcp, @davefalkus, @NickolajA and the Intune product group.
#Most functions is copied from the Powershell Intune Samples: https://github.com/microsoftgraph/powershell-intune-samples
#Script requires the Azure AD Powershell module or the Azure AD Preview Powershell module to run

# Import required modules
try {
  Import-Module -Name AzureAD -ErrorAction Stop
  Import-Module -Name PSIntuneAuth -ErrorAction Stop
}
catch {
  Write-Warning -Message "Failed to import modules"
}

function Get-AuthToken {

  

  [cmdletbinding()]

  param
  (
    [Parameter(Mandatory=$true)]
    $User
  )

  $userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User

  $tenant = $userUpn.Host

  Write-Host "Checking for AzureAD module..."

  $AadModule = Get-Module -Name "AzureAD" -ListAvailable

  if ($AadModule -eq $null) {

    Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview"
    $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable

  }

  if ($AadModule -eq $null) {
    write-host
    write-host "AzureAD Powershell module not installed..." -f Red
    write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow
    write-host "Script can't continue..." -f Red
    write-host
    exit
  }

  # Getting path to ActiveDirectory Assemblies
  # If the module count is greater than 1 find the latest version

  if($AadModule.count -gt 1){

    $Latest_Version = ($AadModule | select version | Sort-Object)[-1]

    $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version }

    # Checking if there are multiple versions of the same module found

    if($AadModule.count -gt 1){

      $aadModule = $AadModule | select -Unique

    }

    $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
    $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"

  }

  else {

    $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
    $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"

  }

  [System.Reflection.Assembly]::LoadFrom($adal) | Out-Null

  [System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null

  $clientId = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547"

  $redirectUri = "urn:ietf:wg:oauth:2.0:oob"

  $resourceAppIdURI = "https://graph.microsoft.com"

  $authority = "https://login.microsoftonline.com/$Tenant"

  try {

    $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority

    # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx
    # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession

    $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto"

    $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId")

    $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result

    # If the accesstoken is valid then create the authentication header

    if($authResult.AccessToken){

      # Creating header for Authorization token

      $authHeader = @{
        'Content-Type'='application/json'
        'Authorization'="Bearer " + $authResult.AccessToken
        'ExpiresOn'=$authResult.ExpiresOn
      }

      return $authHeader

    }

    else {

      Write-Host
      Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red
      Write-Host
      break

    }

  }

  catch {

    write-host $_.Exception.Message -f Red
    write-host $_.Exception.ItemName -f Red
    write-host
    break

  }

}

####################################################

Function Get-AADDevice(){

  

  [cmdletbinding()]

  param
  (
    $DeviceID
  )

  # Defining Variables
  $graphApiVersion = "v1.0"
  $Resource = "devices"

  try {

    $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)?`$filter=deviceId eq '$DeviceID'"

    (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value 

  }

  catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

  }

}

####################################################

Function Get-ManagedDevices(){

  

  [cmdletbinding()]

  param
  (
    [switch]$IncludeEAS,
    [switch]$ExcludeMDM
  )

  # Defining Variables
  $graphApiVersion = "beta"
  $Resource = "deviceManagement/managedDevices"

  try {

    $Count_Params = 0

    if($IncludeEAS.IsPresent){ $Count_Params++ }
    if($ExcludeMDM.IsPresent){ $Count_Params++ }

    if($Count_Params -gt 1){

      write-warning "Multiple parameters set, specify a single parameter -IncludeEAS, -ExcludeMDM or no parameter against the function"
      Write-Host
      break

    }

    elseif($IncludeEAS){

      $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource"

    }

    elseif($ExcludeMDM){

      $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'"

    }

    else {

      $uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'"
      Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices"
      Write-Host

    }

    (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

  }

  catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

  }

}

####################################################

Function Get-ManagedDeviceUser(){

  

  [cmdletbinding()]

  param
  (
    [Parameter(Mandatory=$true,HelpMessage="DeviceID (guid) for the device on must be specified:")]
    $DeviceID
  )

  # Defining Variables
  $graphApiVersion = "beta"
  $Resource = "deviceManagement/manageddevices('$DeviceID')?`$select=userId"

  try {

    $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
    Write-Verbose $uri
    (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).userId

  }

  catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

  }

}

####################################################

# Read credentials and variables

$AADCredential = Get-AutomationPSCredential -Name "WaaSAccount"
$Credential = Get-AutomationPSCredential -Name "WaaSAccount"
$AppClientID = Get-AutomationVariable -Name "AppClientID"
$WindowsVersion = Get-AutomationVariable -Name "WindowsVersion"
$Tenantname = Get-AutomationVariable -Name "Tenantname"
$DefaultGroup = Get-AutomationVariable -Name "DefaultGroup"
$Groups = New-object Microsoft.Open.AzureAD.Model.GroupIdsForMembershipCheck

# Acquire authentication token
try {
    Write-Output -InputObject "Attempting to retrieve authentication token"
    $AuthToken = Get-MSIntuneAuthToken -TenantName $Tenantname -ClientID $AppClientID -Credential $Credential
    if ($AuthToken -ne $null) {
        Write-Output -InputObject "Successfully retrieved authentication token"
    }
}
catch [System.Exception] {
    Write-Warning -Message "Failed to retrieve authentication token"
}

Connect-AzureAD -Credential $AADCredential

#Sets variable for WaaS-groups, Device & User Members of the Self-Service group.

#Sets the Self-Service Group in Script.

$group = Get-AzureADGroup -SearchString "$WindowsVersion-Self-Service Deferred" | Select-Object -ExpandProperty ObjectID
$SearchGroups = Get-AzureADGroup -SearchString "$WindowsVersion-SAC"
$WaaSGroups = Get-AzureADGroup -SearchString "$WindowsVersion-SAC" | Select-Object -ExpandProperty ObjectID
$DeviceMembers = Get-AzureADGroupMember -ObjectId $group -All:$True | Where-Object {$_.ObjectType -eq 'Device'}
$users = Get-AzureADGroupMember -ObjectId $group -All:$True | Where-Object {$_.ObjectType -eq 'User'}  | Get-AzureADUser | Select-Object -ExpandProperty UserPrincipalName 

#Gets all Windows-devices that have each user-member as owner.
#If they already are members they will be skipt and if not the devices will be added to the group

foreach ($user in $users){

  $Groups.GroupIds = $WaaSGroups
  $GroupID = Select-AzureADGroupIdsUserIsMemberOf -ObjectId $user -GroupIdsForMembershipCheck $Groups

  if ($GroupID -ne $null) {
                           $TatooMember = Get-AzureADGroup | Where-Object ObjectID -eq $GroupID | Select-Object -First 1 -ExpandProperty DisplayName
                           Set-AzureADUser -ObjectId $User -FacsimileTelephoneNumber $Tatoomember

        foreach ($UniqueGroupID in $GroupID) {
                                              $MemberRing = Get-AzureADGroup | Where-Object ObjectID -eq $UniqueGroupID | Select-Object -ExpandProperty ObjectID
                                              $MemberUser = Get-AzureADUser -Filter "userPrincipalName eq '$User'" | Select-Object -ExpandProperty ObjectID
                                              Remove-AzureADGroupMember -ObjectId $MemberRing -MemberId $MemberUser
        }

  }

  $devices = Get-AzureADUserOwnedDevice -ObjectId $user | Where-Object {$_.DeviceOSType -eq 'Windows'} | Select-Object -ExpandProperty ObjectID

  foreach ($device in $devices){  

    if ($DeviceMembers -match $device){

      ('{0} is already a member of the group' -f $Device)

    }

    Else{ 

      Add-AzureADGroupMember -ObjectId $group -RefObjectId $device 

    }

  } 

}

#Gets all deferred devices from users in the group and check if they are members of any other WaaS-group. If so, they are removed.

$deferreddevices = $Users | ForEach-Object{

  Get-AzureADUserOwnedDevice -ObjectId $user | Where-Object {$_.DeviceOSType -eq 'Windows'} | Select-Object -ExpandProperty ObjectID

}

foreach ($deferreddevice in $deferreddevices){

  foreach ($WaaSGroup in $WaaSGroups){

    $WaaSMember = Get-AzureADGroupMember -ObjectId $WaaSgroup -All:$True | Where-Object {$_.ObjectType -eq 'Device'} | Select-Object -ExpandProperty ObjectID

    if ($deferreddevice -in $WaaSMember){

      Remove-AzureADGroupMember -ObjectId $WaaSGroup -MemberId $deferreddevice
    }

  }
}

#Gets all devices in the group and compares to the deferred devices. If a device is member, but not have its user in the group, its removed. Also re-adds the user to any previous deployment- or pilot-ring.

foreach ($DeviceMember in $DeviceMembers){

        $MemberObjectID = ($Devicemember | Select-Object -ExpandProperty ObjectID ) 

  if ($MemberObjectID -notin $deferreddevices){

    Remove-AzureADGroupMember -ObjectId $group -MemberId $MemberObjectID

    #If you want to re-add previously deferred machines to a specific group enable the lines below

        $DeviceID = $DeviceMember | Select-Object -ExpandProperty DeviceID
        $IntuneDevice = Get-ManagedDevices -IncludeEAS | Where-Object -Property AzureADDeviceID -EQ $DeviceID  | Select-Object -First 1 -ExpandProperty id
        $OwnerID = Get-ManagedDeviceUser -DeviceID $IntuneDevice
        $Owner = Get-AzureADUser -ObjectID $OwnerID
        $GroupName = $Owner | Select-Object -ExpandProperty FacsimileTelephoneNumber

        if ($GroupName -eq $null) {

                                    $IntuneOwner = Get-AzureADDeviceRegisteredOwner -ObjectId $MemberobjectID | Where-Object -Property UserPrincipalName -NE $Owner.UserPrincipalName
                                    $MultipleOwners = $IntuneOwner.FacsimileTelephoneNumber

                                    if ($MultipleOwners -ne $null) {

                                                $RestoreGroup = Get-AzureADGroup -SearchString $MultipleOwners | Select-Object -ExpandProperty ObjectID
                                                $Groups.GroupIds = $RestoreGroup
                                                $CheckGroup = Select-AzureADGroupIdsUserIsMemberOf -ObjectId $IntuneOwner.UserPrincipalName -GroupIdsForMembershipCheck $Groups

                                                    if ($CheckGroup -eq $Null) {
                                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $IntuneOwner.ObjectId
                                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $MemberObjectID
                                                                               }
                                                    else {
                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $MemberObjectID
                                                          }
                                                          }

                                    else {

                                            if ($DefaultGroup -ne $Null) {
                                                "No previous membership detected, assigning device and owner to default group $DefaultGroup"
                                                 Add-AzureADGroupMember -ObjectId $DefaultGroup -RefObjectId $MemberobjectID
                                                 Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $Owner
                                                 }
                                            else {
                                                'No default restore group assigned, device will be removed from deferral group'
                                                 }
                                                 }
                                    }

        else {

                                                $RestoreGroup = Get-AzureADGroup -SearchString $GroupName | Select-Object -ExpandProperty ObjectID
                                                $Groups.GroupIds = $RestoreGroup
                                                $CheckGroup = Select-AzureADGroupIdsUserIsMemberOf -ObjectId $Owner.UserPrincipalName -GroupIdsForMembershipCheck $Groups

                                                    if ($CheckGroup -eq $Null) {
                                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $Owner.ObjectId
                                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $MemberObjectID
                                                                                }

                                                                                else {
                                                            Add-AzureADGroupMember -ObjectId $RestoreGroup -RefObjectId $MemberObjectID
                                                          }

        }

  }

}

 

Tagged with: , , , , , , , , , , , , , , , ,
Posted in Azure, Education, Intune, Microsoft, Windows 10, Windows as a Service

First tool in the toolbox – Create Software Update rings and groups in Intune

A while ago, to long now, I set out to create a number of scripts to help out with the management of Windows as a Service using Microsoft Intune. Since then, a lot has happened, especially when it comes to the automation capabilities with Intune using the Graph API. Therefore, I’ve now started to re-write the scripts and also adding some additional capabilities to create an, as close to possible, fully automated solution to handle Feature and quality updates with Intune, Azure Automation and Powershell.

Ill continue to add scripts to the toolbox over time, and of course keeping the added once up to date. When I feel that I have enough tools ill combine the implementation of them into an additional post as well, but view these post as a peek into what’s coming. Ill gladly take feature request and welcome feedback.

Before the release of the “complete” solution, ill assume that you have a basic knowledge of Azure Automation, Powershell and Intune – and wont go into detail into how you configure and setup the different solutions – that information will be added later. However, ill of course point out the vital parts, and you can find all the step-by-step guides you need in my previous post: Configure and manage Windows Servicing with Microsoft Intune

So, moving on to the first new tool. Its basically a script to run prior to the release of a new feature upgrade. It will create a number of Software Update rings, as well as corresponding Azure AD Groups, and then assign the corresponding ring to Azure AD Group.

I’m using a number of the publicly available functions from the Intune Product Group based on the Graph API, you can find them on GitHub: Intune Powershell.

I’m also using Nickolaj Andersens Module PSIntuneAuth (read more here) since it enables a smoother experience for Azure Automation.

Pre-reqs:

Azure Automation up and running, with the AzureAD and PSIntuneAuth modules imported, and the following automation variables and credentials  set:

WaaSAccount = The credentials you would like to run the script as (will be used twice)

AppClientID = Your app ID for Intune, use either the default one or set up own of your own.

WindowsVersion = Which version you would like to create rings for.

Tenantname = your tenantname in this format “domain.onmicrosoft.com”

The Script

The script will start by settings a number of variables, import some modules and authenticate. I’ve then configured a number of JSON-templates to be used for configuration of the Update Rings which are added to a variable of their own. Lastly, a second variable is created that includes the names for the groups and rings to be used later on.

The script will first check if policies for this Windows Version exist – and if any of them are missing it will be added. It will then do the the same maneuver with the groups, and when both the groups and the configurations have been created it will assign each respective policy/configuration to the corresponding group.

This script will work nicely with the Self-service deferral script in my previous post, but more scripts will be added later on to complete the solution. I also have a number of improvements lined up for this script as well as the old once. And you’ll find them all at GitHub: WaaS Management

Are there anything you are missing or are in need of when it comes to Windows Servicing with Intune or ConfigMgr? Let me know!


#Author: Simon Binder
#Blog: bindertech.se
#Twitter: @Bindertech
#Thanks to: @daltondhcp, @davefalkus, @NickolajA and the Intune product group.
#Most functions is copied from the Powershell Intune Samples: https://github.com/microsoftgraph/powershell-intune-samples
#Script requires the Azure AD Powershell module or the Azure AD Preview Powershell module to run

Function Test-JSON(){

  

  param (

    $JSON

  )

    try {

    $TestJSON = ConvertFrom-Json $JSON -ErrorAction Stop
    $validJson = $true

    }

    catch {

    $validJson = $false
    $_.Exception

    }

    if (!$validJson){

    Write-Host "Provided JSON isn't in valid JSON format" -f Red
    break

    }

}

Function Add-DeviceConfigurationPolicy(){

  

  [cmdletbinding()]

  param
  (
    $JSON
  )

  $graphApiVersion = "Beta"
  $DCP_resource = "deviceManagement/deviceConfigurations"

    try {

        if($JSON -eq "" -or $JSON -eq $null){

        write-host "No JSON specified, please specify valid JSON for the Android Policy..." -f Red

        }

        else {

        Test-JSON -JSON $JSON

        $uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)"
        Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json"

        }

    }

    catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

    }

}

Function Add-DeviceConfigurationPolicyAssignment(){

  

  [cmdletbinding()]

  param
  (
    $ConfigurationPolicyId,
    $TargetGroupId
  )

  $graphApiVersion = "Beta"
  $Resource = "deviceManagement/deviceConfigurations/$ConfigurationPolicyId/assign"

    try {

        if(!$ConfigurationPolicyId){

        write-host "No Configuration Policy Id specified, specify a valid Configuration Policy Id" -f Red
        break

        }

        if(!$TargetGroupId){

        write-host "No Target Group Id specified, specify a valid Target Group Id" -f Red
        break

        }

        $ConfPolAssign = "$ConfigurationPolicyId" + "_" + "$TargetGroupId"

    $JSON = @"

{
  "deviceConfigurationGroupAssignments": [
    {
      "@odata.type": "#microsoft.graph.deviceConfigurationGroupAssignment",
      "id": "$ConfPolAssign",
      "targetGroupId": "$TargetGroupId"
    }
  ]
}

"@

    $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
    Invoke-RestMethod -Uri $uri -Headers $authToken -Method Post -Body $JSON -ContentType "application/json"

    }

    catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

    }

}

Function Get-SoftwareUpdatePolicy(){

  

  [cmdletbinding()]

  param
  (
    [switch]$Windows10,
    [switch]$iOS
  )

  $graphApiVersion = "Beta"

    try {

        $Count_Params = 0

        if($iOS.IsPresent){ $Count_Params++ }
        if($Windows10.IsPresent){ $Count_Params++ }

        if($Count_Params -gt 1){

        write-host "Multiple parameters set, specify a single parameter -iOS or -Windows10 against the function" -f Red

        }

        elseif($Count_Params -eq 0){

        Write-Host "Parameter -iOS or -Windows10 required against the function..." -ForegroundColor Red
        Write-Host
        break

        }

        elseif($Windows10){

        $Resource = "deviceManagement/deviceConfigurations?`$filter=isof('microsoft.graph.windowsUpdateForBusinessConfiguration')&amp;`$expand=groupAssignments"

        $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
        (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).value

        }

        elseif($iOS){

        $Resource = "deviceManagement/deviceConfigurations?`$filter=isof('microsoft.graph.iosUpdateConfiguration')&amp;`$expand=groupAssignments"

        $uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
        (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value

        }

    }

    catch {

    $ex = $_.Exception
    $errorResponse = $ex.Response.GetResponseStream()
    $reader = New-Object System.IO.StreamReader($errorResponse)
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    $responseBody = $reader.ReadToEnd();
    Write-Host "Response content:`n$responseBody" -f Red
    Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
    write-host
    break

    }

}

# Import required modules
try {
    Import-Module -Name AzureAD -ErrorAction Stop
    Import-Module -Name PSIntuneAuth -ErrorAction Stop
}
catch {
    Write-Warning -Message "Failed to import modules"
}

# Read credentials and variables
$AADCredential = Get-AutomationPSCredential -Name "WaaSAccount"
$Credential = Get-AutomationPSCredential -Name "WaaSAccount"
$AppClientID = Get-AutomationVariable -Name "AppClientID"
$WindowsVersion = Get-AutomationVariable -Name "WindowsVersion"
$Tenantname = Get-AutomationVariable -Name "Tenantname"

# Acquire authentication token
try {

    Write-Output -InputObject "Attempting to retrieve authentication token"
    $AuthToken = Get-MSIntuneAuthToken -TenantName $Tenantname -ClientID $AppClientID -Credential $Credential

    if ($AuthToken -ne $null) {

        Write-Output -InputObject "Successfully retrieved authentication token"
    }
}

catch [System.Exception] {

    Write-Warning -Message "Failed to retrieve authentication token"
}

Connect-AzureAD -Credential $AADCredential

#Enter the Windows 10 version you want to deploy in $WindowsVersion. Change the names (or add additional rings) as you like, but remember to change any other corresponding scripts.

$WaaSGroups = "$WindowsVersion-SAC - Technical", "$WindowsVersion-SAC - Compatibility", "$WindowsVersion-SAC - First Ring", "$WindowsVersion-SAC - Second Ring", "$WindowsVersion-Compatibility Issues", "$WindowsVersion-Self-Service Deferred"  

$NumberofGroups = $WaaSGroups.Count

$i = 0

$JSONConfigurations = @"

    {

    "displayName":"$WindowsVersion-SAC - Technical",
    "description":"$WindowsVersion-SAC - Technical",
    "@odata.type":"#microsoft.graph.windowsUpdateForBusinessConfiguration",
    "businessReadyUpdatesOnly":"all",
    "microsoftUpdateServiceAllowed":true,
    "driversExcluded":false,
    "featureUpdatesDeferralPeriodInDays":0,
    "qualityUpdatesDeferralPeriodInDays":0,
    "automaticUpdateMode":"autoInstallAtMaintenanceTime",
    "deliveryOptimizationMode":"httpWithInternetPeering",

        "installationSchedule":{
        "@odata.type":"#microsoft.graph.windowsUpdateActiveHoursInstall",
        "activeHoursStart":"08:00:00.0000000",
        "activeHoursEnd":"17:00:00.0000000"
        }

    }

"@, @"

    {

    "displayName":"$WindowsVersion-SAC - Compatibility",
    "description":"$WindowsVersion-SAC - Compatibility",
    "@odata.type":"#microsoft.graph.windowsUpdateForBusinessConfiguration",
    "businessReadyUpdatesOnly":"all",
    "microsoftUpdateServiceAllowed":true,
    "driversExcluded":false,
    "featureUpdatesDeferralPeriodInDays":30,
    "qualityUpdatesDeferralPeriodInDays":3,
    "automaticUpdateMode":"autoInstallAtMaintenanceTime",
    "deliveryOptimizationMode":"httpWithPeeringNat",

        "installationSchedule":{
        "@odata.type":"#microsoft.graph.windowsUpdateActiveHoursInstall",
        "activeHoursStart":"08:00:00.0000000",
        "activeHoursEnd":"17:00:00.0000000"
        }

    }

"@, @"

    {

    "displayName":"$WindowsVersion-SAC - First Ring",
    "description":"$WindowsVersion-SAC - First Ring",
    "@odata.type":"#microsoft.graph.windowsUpdateForBusinessConfiguration",
    "businessReadyUpdatesOnly":"all",
    "microsoftUpdateServiceAllowed":true,
    "driversExcluded":false,
    "featureUpdatesDeferralPeriodInDays":90,
    "qualityUpdatesDeferralPeriodInDays":5,
    "automaticUpdateMode":"autoInstallAtMaintenanceTime",
    "deliveryOptimizationMode":"httpWithPeeringNat",

        "installationSchedule":{
        "@odata.type":"#microsoft.graph.windowsUpdateActiveHoursInstall",
        "activeHoursStart":"08:00:00.0000000",
        "activeHoursEnd":"17:00:00.0000000"
        }

    }

"@, @"

    {

    "displayName":"$WindowsVersion-SAC - Second Ring",
    "description":"$WindowsVersion-SAC - Second Ring",
    "@odata.type":"#microsoft.graph.windowsUpdateForBusinessConfiguration",
    "businessReadyUpdatesOnly":"all",
    "microsoftUpdateServiceAllowed":true,
    "driversExcluded":false,
    "featureUpdatesDeferralPeriodInDays":120,
    "qualityUpdatesDeferralPeriodInDays":7,
    "automaticUpdateMode":"autoInstallAtMaintenanceTime",
    "deliveryOptimizationMode":"httpWithPeeringNat",

        "installationSchedule":{
        "@odata.type":"#microsoft.graph.windowsUpdateActiveHoursInstall",
        "activeHoursStart":"08:00:00.0000000",
        "activeHoursEnd":"17:00:00.0000000"
        }

    }

"@, @"

    {

    "displayName":"$WindowsVersion-Compatibility Issues",
    "description":"$WindowsVersion-Compatibility Issues",
    "@odata.type":"#microsoft.graph.windowsUpdateForBusinessConfiguration",
    "businessReadyUpdatesOnly":"all",
    "microsoftUpdateServiceAllowed":true,
    "driversExcluded":false,
    "featureUpdatesDeferralPeriodInDays":180,
    "qualityUpdatesDeferralPeriodInDays":30,
    "automaticUpdateMode":"autoInstallAtMaintenanceTime",
    "deliveryOptimizationMode":"httpWithPeeringNat",

        "installationSchedule":{
        "@odata.type":"#microsoft.graph.windowsUpdateActiveHoursInstall",
        "activeHoursStart":"08:00:00.0000000",
        "activeHoursEnd":"17:00:00.0000000"
        }

    }

"@, @"

    {

    "displayName":"$WindowsVersion-Self-Service Deferred",
    "description":"$WindowsVersion-Self-Service Deferred",
    "@odata.type":"#microsoft.graph.windowsUpdateForBusinessConfiguration",
    "businessReadyUpdatesOnly":"all",
    "microsoftUpdateServiceAllowed":true,
    "driversExcluded":false,
    "featureUpdatesDeferralPeriodInDays":140,
    "qualityUpdatesDeferralPeriodInDays":21,
    "automaticUpdateMode":"autoInstallAtMaintenanceTime",
    "deliveryOptimizationMode":"httpOnly",

        "installationSchedule":{
        "@odata.type":"#microsoft.graph.windowsUpdateActiveHoursInstall",
        "activeHoursStart":"08:00:00.0000000",
        "activeHoursEnd":"17:00:00.0000000"
        }

    }

"@

foreach ($JSON in $JSONConfigurations) {

    $PolicyName = $WaaSGroups | Select-Object -Index $i

    $Policyexist = Get-SoftwareUpdatePolicy -Windows10 | Where-Object -Property Displayname -EQ $PolicyName

    if ($Policyexist -eq $null) {

                                  Write-Host "$PolicyName do not exist, creating $PolicyName"

                                  try {

                                        Add-DeviceConfigurationPolicy -JSON $JSON

                                      }

                                Catch {

                                        Write-Host "Unable to create the policy: $PolicyName, script will terminate"
                                        Write-Host "$_.Exception.Message"
                                        Break

                                      }

                                 }

                            else {

                                  Write-Host "$PolicyName exist, moving to assignment"

                                 }
                            $i++

}

foreach ($WaaSGroup in $WaaSGroups){

    try {

        $Groupexist = Get-AzureADGroup -SearchString $WaaSGroup | Select-Object -ExpandProperty ObjectID

        if ($Groupexist -eq $null) {

                                    Write-Host "$WaaSGroup do not exist, creating $WaaSGroup"

                                    try {

                                        New-AzureADGroup -Description "$WaaSGroup" -DisplayName "$WaaSGroup" -SecurityEnabled $true -MailEnabled $false -MailNickName 'NotSet'
                                        Start-Sleep -Seconds 5
                                        $Groupexist = Get-AzureADGroup -SearchString $WaaSGroup | Select-Object -ExpandProperty ObjectID

                                        }

                                    Catch {

                                          "Unable to create $WaaSGroup, script will terminate"
                                          Break

                                          }

                                         }

                                    else {

                                          Write-Host "$WaaSGroup exist, moving to assignment"

                                          }

                                }

                Finally {

                        $PolicyID = Get-SoftwareUpdatePolicy -Windows10 | Where-Object -Property Displayname -EQ $WaaSGroup | Select-Object -ExpandProperty id
                        Add-DeviceConfigurationPolicyAssignment -ConfigurationPolicyId $PolicyID -TargetGroupId $GroupExist

                        }

}<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;">&#65279;</span>
Tagged with: , , , , , , , , , , , , , , ,
Posted in Azure, Intune, Microsoft, Windows 10, Windows as a Service

Episode 30 – Presentation Remoote

The thirtieth episode is up, a.k.a the “presentation remoote” episode!

We talk about what’s behind the scenes of Power BI desktop, SQL Saturday Vienna (which still was held on a Friday, by the way), new builds of InTune, Windows and Windows Server, the Nordic Infrastructure conference and some presentation technique tips thrown in for good measure.

Knee-deep in Tech on iTunes

Episode 30

Tagged with: , , , , , , , , , , , , ,
Posted in Microsoft, News, Podcast, Power BI, Public Speaking, SCCM, System Center, Windows 10, Windows as a Service, Windows Insider, Windows Server

Episode 29 – On the ball

The twentyninth (and first episode of 2018!) episode is up, a.k.a the “on the ball” episode!

This time we talk shenanigans with PowerBI and ODBC connections, WANem for WAN emulation, Meltdown and Spectre, yet another insider build and what’s in store for 2018.

We’ve switched over to Pippa.io for file hosting. Expect some small technical glitches while we get settled in 🙂

If you have any feedback or would like to connect with us, just ping me (@Bindertech) or Alexander (@Arcticdba)!

Knee-deep in Tech on iTunes

Episode 29

https://player.pippa.io/kneedeepintech/episodes/episode-29

Tagged with: , , , , , ,
Posted in Azure, Hardware, Intune, Microsoft, News, Podcast, Power BI, Windows 10
I’m speaking!
I’m going!
Follow me on Twitter!