Getting stats on AD Groups

Sup’ PSHomies,

Did you ever had the need to “know” just who are members of a specific group and more importantly, if they were users or groups… Maybe I can assist… 😉

To distinguish between a user or group use Get-ADUser / Get-ADGroup. Get-ADUser will process None or Microsoft.ActiveDirectory.Management.ADUser only objects. The same goes for Get-ADGroup. Either None or Microsoft.ActiveDirectory.Management.ADGroup only objects will be processed.

With this info we can now process the AD Group to get just who is a member and what is their object class.

#region Get AD Group stats
#Specify ADGroup(s) using like
$groupName = 'DAT_ICT*'
$adGroup = Get-ADGroup -Filter { Name -like $groupName} |
Foreach-Object {
[PSCustomObject]@{
Group = $_
GroupMembers = Get-ADGroup -Filter { memberOf -eq $_.DistinguishedName }
UserMembers = Get-ADUser -Filter { memberOf -eq $_.DistinguishedName }
UserMembersRecursive = Get-ADGroup -Filter { memberOf -eq $_.DistinguishedName } |
ForEach-Object{
Get-ADGroupMember -Identity $_ -Recursive
}
GroupMemberOf = Get-ADGroup -Filter { members -eq $_.DistinguishedName }
}
}
#Get Count of the ADGroup(s)
$adGroupMembersCount = $adGroup |
ForEach-Object{
[PSCustomObject]@{
Group = $_.Group.Name
countGroupMembers = @($_.GroupMembers).Count
countUserMembers = @($_.UserMembers).Count
countUserMembersRecursiveUnique = @($_.UserMembersRecursive | Select-Object -Unique ).Count
countGroupMembersOf = @($_.GroupMemberOf).Count
}
}
#endregion

I opted for a specific AD Group name pattern. Feel free to refactor to your needs! Depending on your AD size it may take a while if you decide to retrieve all AD Groups…

Hope it’s worth something to you,

Ttyl,

Urv

YaExOs (Yet another Exchange Online script)

Sup’ PSHomies,

Exchange online… I’ll be honest… In my many years in IT, I avoided Exchange like the plague! I’ve seen many colleagues crumble under it’s weight! (Sounds more dramatic don’t you think?) But with Office 365 being all the rage now, I guess I better get with the program, so here goes…

So there was recently a bit of a commotion on twitter about sharing level 200 stuff. We all were beginners at some point, so if you already know this then I guess this blog wasn’t meant for you… 😉

So how do you go about creating a mailbox for a user in Office 365 Exchange? Using PowerShell of course… Hehe…

Ok there are modules for AzureAD and MSOnline and stuff but where are the Exchange Online cmdlets? That was my first step. Luckily I found an excellent blog on how to go about doing this. The rest after that is pretty straightforward…

Looking at the cmdlet I wanted to figure out if there were any mandatory attributes.

(get-command New-OnlineMailbox).ParameterSets.Parameters | Out-GridView

 

No mandatory parameter(s)… I’ll just stick with the basics on this one:

  • Alias
  • Name
  • FirstName
  • LastName
  • DisplayName

Feel free to add any other parameters you need… 😉

You’ll need Global Admin credentials to create your Mailbox Online so have that at hand…

Once the Mailbox is created you’ll need to assign a license to the user. I’m using the MSOL cmdlet for this just because it’s still around (and way easier to implement). Having said that here’s a link to how it’s done using AzureAD cmdlets. That practical365 site is definitely a good source for anything Office365 related! 😉 Oh and the book is definitely worth having as a reference too!

Ok here’s the script:

<#
Author: I. Strachan
Version: 1.0
Version History:
Purpose: Import Exchange Online cmdlets and create a New Mailbox
#>
param(
[String]
$Alias = 'irwins',
[String]
$Name = 'irwins',
[String]
$FirstName = 'Irwin',
[String]
$LastName = 'Strachan',
[String]
$DisplayName = 'Irwin Strachan'
)
#region Exchange Online Functions
#https://practical365.com/exchange-server/powershell-function-connect-office-365/
function Connect-EXOnline {
Param(
[PSCredential]$EXOnlineCred
)
$Url = 'https://ps.outlook.com/powershell&#39;
$paramEXOSession = @{
ConfigurationName = 'Microsoft.Exchange'
ConnectionUri = $Url
Credential = $EXOnlineCred
Authentication = 'Basic'
AllowRedirection = $true
Name = 'Exchange Online'
}
$EXOSession = New-PSSession @paramEXOSession
Import-PSSession $EXOSession -Prefix 'Online'
}
function Disconnect-EXOnline {
Remove-PSSession -Name 'Exchange Online'
}
#endregion
#region Get Credentials and Connect To Exchange Online
$paramCredential = @{
Message = 'Enter Global O653 Admin account'
UserName = 'o365admin@yourtenant.onmicrosoft.com'
}
$o365Admin = Get-Credential @paramCredential
Connect-EXOnline -EXOnlineCred $o365Admin
Connect-MsolService -Credential $o365Admin
#endregion
#region Create a new Mailbox
$paramEXOMailBox = @{
Alias = $Alias
Name = $Name
FirstName = $FirstName
LastName = $LastName
DisplayName = $DisplayName
MicrosoftOnlineServicesID = '{0}@yourtenant.onmicrosoft.com' -f $Alias
Password = $(ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force)
ResetPasswordOnNextLogon = $true
}
New-OnlineMailbox @paramEXOMailBox
#endregion
#region Assign new Mailbox a license
#Get AccountSku. In my case I only have one
$SkuId = (Get-MsolAccountSku).AccountSkuId
#Get All unlicensed users
Get-MsolUser -All -UnlicensedUsersOnly
#Set MSOL User License
#Alternatively have a look at https://practical365.com/blog/managing-office-365-licenses-with-azure-ad-v2-powershell-module/
Set-MsolUser -UserPrincipalName $paramEXOMailBox.MicrosoftOnlineServicesID -UsageLocation "NL"
Set-MsolUserLicense -UserPrincipalName $paramEXOMailBox.MicrosoftOnlineServicesID -AddLicenses $SkuId
#endregion
#region Disconnect
Disconnect-EXOnline
#endregion

So that’s how to get started with Exchange Online.

I’ll leave you with a lil’ thought from the Austrian Oak himself…

Beginners

Hope it’s worth something to you,

Ttyl,

Urv

 

 

Reverse engineering email address from OneDrive personal site Url

Sup’ PSHomies,

Migrations… Gotta love ’em!!!

I’m doing a OneDrive for Business tenant to tenant migration using Sharegate. If you ever decide to do any sort of SharePoint affiliated migration, best to use Sharegate! Their support is top notch! Shout out to Jimmy De Santis!

Sharegate has a cmdlet Get-OneDriveUrl that will retrieve or provision you ODFB URL (OneDrive For Business). Depending on a user’s Primary SMTP Address the URL format may not be what you expected.

Here’s a link to the script to get your ODFB information of your organization.  Be sure to follow the setup instructions.

This will generate a simple text file (I used it as-is, a rare occasion, I know… Don’t judge me…). To generate the URL, you need to swap any ‘.’ and ‘@’ for ‘_’.  I’m looking to reverse this process. Why? Well because I hate manual work. I’m almost certain I have to do this again sometime in the near future…

Those who forget to automate are doomed to repeat their work…

Words to live by…

My hunch paid off! By reverse engineering the email address from the ODFB URL, I could use that email address to retrieve the ODFB URL using Get-OneDriveUrl. I needed to provision the URLs in the target domain, fortunately the email addresses in the Target tenant are uniform.

Ok here’s the code:

$personalSites = @"
personalSite
/personal/a_v_d_hoogt_simba_nl/
/personal/h_altafah_simba_nl/
/personal/c_v_d_meulen_simba_nl/
/personal/stand_simba_nl/
/personal/floortje_janssen_simba_nl/
/personal/j_dubendorffer_sudoku_nl/
/personal/m_d_haas_sudoku_nl/
/personal/j_slimmer_sudoku_nl/
/personal/m_rijs_sudoku_nl/
/personal/b_janssen_despacito_com/
/personal/m_pools_despacito_com/
/personal/r_stewardess_despacito_com/
/personal/t_koestsier_despacito_com/
"@ | ConvertFrom-Csv
$personalSites |
ForEach-Object {
#Get ODFB part
Try {
$arrODFBUrl = ($($_.personalSite).Split('//'))[-2].Split('_')
$count = @($arrODFBUrl).Count
$index = 1
$emailUsedForODFB = $arrODFBUrl[0]
Do {
if ($arrODFBUrl[$index] -like 'despacito*') {
$emailUsedForODFB = "$($emailUsedForODFB)@$($arrODFBUrl[$index])"
}
elseif ($arrODFBUrl[$index] -like 'simba*') {
$emailUsedForODFB = "$($emailUsedForODFB)@$($arrODFBUrl[$index])"
}
elseif ($arrODFBUrl[$index] -like 'sudoku*') {
$emailUsedForODFB = "$($emailUsedForODFB)@$($arrODFBUrl[$index])"
}
else {
$emailUsedForODFB = "$($emailUsedForODFB).$($arrODFBUrl[$index])"
}
$index++
}until($index -eq $count)
#Some ODFB may have a '1' at the end. Just remove that
if ($emailUsedForODFB -like '*nl1') {
$emailUsedForODFB = $emailUsedForODFB -replace ".$"
}
$samAccountName = $emailUsedForODFB.Split('@')[0].ToLower()
$targetEmail = '{0}@urv.onmicrosoft.com' -f $samAccountName
[PSCustomObject]@{
SamAccountName = $samAccountName
SourceEmail = $emailUsedForODFB
SourcePersonalUrl = $($_.personalSite)
SourceGeneratedUrl = 'https://irwins-my.sharepoint.com/personal/{0}/' -f $emailUsedForODFB.Replace('@', '_').Replace('.','_').ToLower()
TargetEmail = $targetEmail
TargetGeneratedUrl = 'https://urv-my.sharepoint.com/personal/{0}/' -f $targetEmail.Replace('@', '_').Replace('.','_').ToLower()
}
}
catch {
Write-Warning 'Not a valid personal site format'
}
} |
Out-GridView

Having the email address I can generate the source URL and verify if it is what I expected it to be. The samaccountname remained the same, so it was easy enough to generate the new email address and the expected target URL which once I provision, can also be verified that it is what I expected it to be. Still with me? Just have a look at the image… 😉

Generated ODFB Urls

You can generate quite the information by just have a personal ODFB Url!

Not being content with just the generated information, I did one last check using Sharegate’s Get-OneDriveUrl:

$csvUrls = Import-Csv C:\scripts\sources\csv\VerifyUrls.csv -Delimiter "`t" -Encoding UTF8
#region Initialize Stuff
Import-Module ShareGate -Verbose:$false
# User account for an Office 365 global admin in your organization
# https://www.jaapbrasser.com/quickly-and-securely-storing-your-credentials-powershell/
$HashCreds = Import-CliXml -Path "${env:\userprofile}\Hash.Cred"
#Admin urls
$targetAdminUrl = 'https://urv-admin.sharepoint.com&#39;
$sourceAdminUrl = 'https://irwins-admin.sharepoint.com&#39;
#Connect to Tenant Site
$targetTenantSite = Connect-Site -Url $targetAdminUrl -Credential $HashCreds.'urv-target-o365admin'
$sourceTenantSite = Connect-Site -Url $sourceAdminUrl -Credential $HashCreds.'irwins-source-o365admin'
Function Get-ODFBUrls{
param($sourceUserEmail,$targetUserEmail)
#Provision using Sharegate and wait for it to finish.
$targetOneDriveUrl = Get-OneDriveUrl -Tenant $targetTenantSite -Email $targetUserEmail
$sourceOneDriveUrl = Get-OneDriveUrl -Tenant $sourceTenantSite -Email $sourceUserEmail
[PSCustomObject]@{
SourceEmail = $sourceUserEmail
SourceODFBUrl = $sourceOneDriveUrl
TargetEmail = $targetUserEmail
TargetODFBUrl = $targetOneDriveUrl
}
}
#region Get ODFB Urls
$csvUrls |
ForEach-Object{
Get-ODFBUrls -sourceUserEmail $_.SourceEmail -targetUserEmail $_.TargetEmail
}|
Export-Csv C:\scripts\export\ODFBUrls-27112017.csv -Encoding UTF8 -Delimiter "`t" -NoTypeInformation
#endregion

This aligns the source and target URLs nicely! Now I can migrate from tenant to tenant without to much hassle… Well at least that’s the plan… 😉

Hope it’s worth something to you,

Ttyl,

Urv

Taking ADUser validation a step further…

‘Sup PSHomies,

This is me every single time I sit down to use Pester…

PesterOVF.PNG

Hehe…

While I do enjoy using Pester for operational validation, what do you do with the ones that fail? Most of the time you’re the one doing the validation in the chain of process and everything goes right! Because you’re that good… 😉 Hehe…  I’ve been recently asked to update some users where some attributes didn’t get populated during a migration… Are you thinking what I’m thinking? 😉

The first thing I did was export the attributes they needed, easy enough. Next was to change the necessary attributes to the correct value. The ones that differ will fail (Of course they will Urv, not a total retard here… get on with it… )

Here’s where having proper descriptions can go a long way.

Breadcrumbing

This is what a Test ErrorRecord looks like when you capture the results:PesterError

At first I thought of using regex on the ErrorRecord. Some of the  attributes aren’t set which gave me some issues, so I decided to breadcrumb the name. First do a split on the colon ‘:’ grab the last part and do a split again using ‘/’ to get the attribute name and value. Don’t forget to trim() 🙂

There were some other Attributes that weren’t part of the default parameter set of the Set-ADUser cmdlet. To change those attributes you need to use the DisplayName and the Replace Operator. For the attributes “not set” that need to be cleared, use the Clear operator. Just don’t use both the parameter and the DisplayName! I had EmailAddress and mail in the CSV file, one passed and the other failed… I got rid of mail…

Ok here’s the code to get things done:

First get failed Pester tests.

<#
Author: I. Strachan
Version:
Version History:
Purpose: Validate AD User attributes
#>
[CmdletBinding(SupportsShouldProcess = $True)]
Param(
$csvFile = 'd:\scripts\ps1\source\csv\SetUser.csv'
)
#region Import Csv file
$csvUsers = Import-Csv -Path $csvFile -Delimiter "`t" -Encoding UTF8
$userProperties = ($csvUsers | Get-Member -MemberType NoteProperty).Name |
Where-Object {$_ -ne 'OU'}
#endregion
#region Main
$csvUsers |
Foreach-Object {
$Expected = $_
Describe "Processing User: $($Expected.SamAccountName)" {
Context "Verifying AD User properties for $($Expected.DisplayName)" {
#Get AD user properties
$Actual = Get-ADUser -Identity $Expected.SamAccountName -Properties $userProperties
ForEach( $property in $userProperties){
if (([string]::isNullOrEmpty($Expected.$property))) {
$Expected.$property = $null
$lableExpected = '<not set>'
}
else{
$lableExpected = $Expected.$property
}
it "Verifying user property: $($property) / $($lableExpected)"{
$Actual.$property | Should be $Expected.$property
}
}
}
}
}
#endregion
#Run PesterTest and save results
$resultsTest = Invoke-Pester D:\scripts\ps1\dsa\ADUser.Properties.Tests.ps1 -PassThru
#Get All failed tests
$failedTests = $resultsTest.TestResult.where{$_.Passed -eq $false}
$failedTests |
ForEach-Object{
$result = $_.Name.Split(':')[-1]
$arrResult = $result.Split('/')
[PSCustomObject]@{
SamAccountName = ($_.Describe.split(':').Trim())[-1]
Property = $arrResult[0].Trim()
Expected = $arrResult[1].Trim()
}
} -OutVariable failedObjects
#Export Failed objects
$failedObjects |
Export-Csv -Path D:\Scripts\ps1\source\csv\FailedTests.csv -Delimiter "`t" -NoTypeInformation -Encoding UTF8

This will give me a csv file with the following Columns, SamAccountName, Property & Expected (Value).

The set failed Pester tests will either set or clear the attribute depending on it’s value. If it’s $null it will be cleared.

<#
Author: I. Strachan
Version:
Version History:
Purpose: Set ADUser attributes of failed tests
#>
[CmdletBinding(SupportsShouldProcess = $True)]
Param(
$csvFile = 'D:\Scripts\ps1\source\csv\FailedTests.csv'
)
$FailedTests = Import-Csv -Path $csvFile -Delimiter "`t" -Encoding UTF8
#Get Set-ADUser Parameters
$setADUserParameters = (Get-Command Set-ADUser).ParameterSets.Parameters.Where{ $_.IsDynamic -eq $true} | Select-Object -ExpandProperty Name
#Get User Property
$FailedTests |
Foreach-object{
#Set Expected to null if <not set>
$Expected = @{$true = $null; $false = $_.Expected}[$_.Expected -eq '<not set>']
If ($setADUserParameters.Contains($_.Property)){
$paramSetADUser = @{
Identity = $_.SamAccountName
$_.Property = $Expected
}
Set-ADUser @paramSetADUser
}
else{
if($Expected){
Set-ADUser -Identity $($_.SamAccountName) -Replace @{$_.Property = $Expected}
}
else{
Set-ADUser -Identity $($_.SamAccountName) -Clear $($_.Property)
}
}
}

Here are some screenshots to give you an idea what to expect.

Ideally you’d only have a few failed tests. I wouldn’t use this to reset entire User attributes. Moving and/or renaming an object isn’t supported… yet! 😉

So there you have it, taking failed Pester tests values and setting them accordingly!

Hope it’s worth something to you,

Ttyl,

Urv

 

 

Microsoft Teams MessageCards

‘Sup PSHomies,

I was excited to give the MS Teams module a go! I was secretly hoping for a cmdlet to send messages to channels. Unfortunately no such luck… yet! If push comes to shove you can always take the Graph route! 😉

I posted the blog on social media and that generated some interesting ideas. Emotions and opinions varied quite a bit. Prateek raised an interesting question on whether slack would become obsolete in the near future. MS Teams is gaining momentum and it integrates nicely in Microsoft’s eco system, so why wouldn’t you use it? It doesn’t have to be black or white, use whatever adds value to your business I’d say!

This is where social media can be a treasure trove at times, got an interesting tip from Brett Miller on posting messages to channels using webhooks! Ah! the missing cmdlet I was hoping for! “So basically it’s just an Invoke-RestMethod?” Cool! I gave it a spin:

New-TeamMessage

Nice! Hmmm… Webhooks eh? Sounds familiar… Where did I read about that? Ah yes! Stefan Stranger did a blog about that. To be honest, I did read his blog, but at that time I didn’t have access to Teams… 😉 Just follow his instructions and you’re golden!

Here’s when the fun really started! Come to find out that there’s a lot more you can post using the message card reference! There’s even a card playground where you can try out your message cards. It’s all JSON, no problem, we’ve got cmdlets for that.

There’s a whole design guide on Messagecards. Best of all it supports MarkDown format! I tried a sending a table but that didn’t work as planned.

Just imagine the automation possibilities here… I understand now why ChatOps is really taking off…

My first attempt at DSL

Looking at the MessageCard format I thought: “This would be interesting as a DSL (Domain Specific Language) implementation… Hmmm…” I’ve been meaning to look into DSL after reading Kevin Marquette’s blog series on the subject. Seems now is a good time as any to start! This is what I came up with so far… It’s a work in progress ( sharing is caring) , but it works!

#region MessageCard function helpers
function MessageCard {
param([scriptblock]$ScriptBlock)
$newScript = "@{$($ScriptBlock.ToString())}"
$newScriptBlock = [scriptblock]::Create($newScript)
& $newScriptBlock
}
function section {
param([scriptblock]$ScriptBlock)
$newScript = "[Ordered]@{$($ScriptBlock.ToString())}"
$newScriptBlock = [scriptblock]::Create($newScript)
& $newScriptBlock
}
function fact {
param([scriptblock]$ScriptBlock)
$Invoked = $ScriptBlock.Invoke()
$Invoked.Keys |
ForEach-Object {
@{
Name = $_
Value = $Invoked.$_
}
}
}
#endregion
#region Main
$NewMessage = MessageCard {
summary = 'This is the summary property'
title = "This is the card's title property"
text = 'This is the cards text property.'
sections = @(
section {
activityTitle = 'Activity Title'
activitySubtitle = 'Activity **Sub**Title'
images = @(
@{
image = "http://connectorsdemo.azurewebsites.net/images/WIN12_Scene_01.jpg&quot;
title = "This is the image alternate text Pic 01"
}
@{
image = "http://connectorsdemo.azurewebsites.net/images/WIN12_Anthony_02.jpg&quot;
title = "This is the image alternate text Pic 02"
}
)
PotentialAction = @(
@{
'@type' = 'Actioncard'
Name = 'Comment'
Inputs = @(
@{
'@type' = 'TextInput'
Id = 'Comment'
isMultiLine = $true
Title = 'Input title property'
}
)
Actions = @(
@{
'@type' = 'HttpPOST'
Name = 'Save'
Target = 'http://...'
}
)
}
@{
'@type' = 'Actioncard'
Name = 'Due Date'
Inputs = @(
@{
'@type' = 'DateInput'
Id = 'dueDate'
Title = 'Input due date'
}
)
Actions = @(
@{
'@type' = 'HttpPOST'
Name = 'Save'
Target = 'http://...'
}
)
}
)
}
section {
title = 'Details:'
facts = fact {
@{
GivenName = 'Irwin'
SurName = 'Strachan'
}
}
}
section {
activityTitle = 'Default MD Support'
activitySubtitle = 'Activity **Subtitle**'
facts = fact {
@{
Email = 'Irwin@DosSantos.ca'
WebSite = '[pshirwins](https://pshirwin.wordpress.com)'
}
}
}
)
}
$restparams = @{
Uri = "Your WebHook Uri"
Method = 'POST'
Body = $($NewMessage | ConvertTo-Json -Depth 6)
ContentType = 'application/JSON'
}
Invoke-RestMethod @restparams
#endregion

And here’s the result:

New-MessageCard

I used the full card format as reference.  I realize that others in the community could easily knock this out the park. Maybe we can make this a community project? I would love to see what the possibilities are here…

Turns out not having a Send-TeamMessage wasn’t as painful as I thought it would be. Many ways to Rome…

Shout out to Brett & Stefan! Gotta love the PowerShell community!!!

Hope it’s worth something to you,

Ttyl,

Urv

PSConfAsia 2017

I recently had the pleasure of presenting at PSConf.asia in Singapore. This was my very first conference as a speaker! As a first time speaker a great way to get started.

Jaap Brasser approached me at PSConf.Eu about doing a session (or two) at PSConf.asia. At first I was hesitant…  me, a speaker… Hmmm… I need to think about… Let me get back to you on that… Jaap was clever enough to appease my ego, now how could I say no?

I decided to get out of my comfort zone and said yes, much to Jaap’s delight! Alright! Another PSConf, this time in Singapore, how cool is that!

So after I said yes I did some googling… Let’s see Singapore… Wait what? 12 Hours??? Oh boy… Flying isn’t really my favorite passtime…

ba plane

Hehe… Fortunately I had night flights so…

Singapore has my kinda weather, tropical! I came well prepared. Poor Rob Sewell was melting in Singapore. Transportation is awesome in Singapore. From and to the conference was about 1 SGD.

The organizers did an awesome job! Great location! I met up with old acquaintances and made some new ones!

Finally got to interact with David das Neves. I wanted to attend his session but I opted for Steve Hosking session on Graph. In case you missed it Graph is going to interconnect everything. Definitely have a look at it. I liked David’s approach on having a Class for different types of log format. So I asked him “Hey no  love for Robocopy?” Next thing I knew it was in it! You can read all about hereRaimund Andrée was also there. I told him how NTFSSecurity module saved me on quite a few occasions. He has some serious plans for the module on PowerShell core, so stay tuned!

Rick Taylor won a book on Windows PowerShell for Developers by Doug Finke! Rick was like: “Wait I know this guy!” So I said we need to take a pic featuring Doug’s book! :-). Max Trinidad is a great guy! Been in IT for some time and has loads and loads of skills and it definitely shows!

The keynote is always the highlight of the Conference. Angel Calvo delivered the keynote like a boss! How can I do the key note justice? It’s all about Digital Transformation.

Digital Transformation

Digital tranformation

This is something we all need to consider. It’s not a question of if but when and how. If you’re content with where you’re at right now then you will be obsolete in the near future. What I really appreciated, was the fact that Angel acknowledged that this change can be overwhelming, but it isn’t an all-or-nothing situation. Start where you can, with what you can and take it from there. This digital transformation may take you places you never thought possible. I spoke to Amanda Debler , her transformation has her working more with kurbernetes, how cool is that?

Azure is about adding value to your business

If you’re still under the impression that Azure is just for offloading servers to the cloud, you’re sorely mistaken. We got some excellent demos by Micheal Greene and Ravi Kiran Chintalapudi on Azure Management Services. Azure is about delivering value to your business. And what has value? Data! Lots and lots of it! Having your servers, application, services just to name a few in Azure, will give you the chance to transform metric and monitoring data into added value for your business. As an OPS guy I always thought of monitoring as a necessary evil. Ravi showed us how to manipulate Azure data that is at your deposition and make it valuable! That one server that isn’t patched adequately or that server that’s missing a configuration, the data is there, it’s up to you to turn that into valuable information.

ChatOps

Michael Greene’s demo on ChatOps blew my mind!

ChatOps

I’ve seen chatbots in action but this will take things to the next level! Here are a few links Michael shared to give you an idea where they’re heading:

PowerShell and the future

When Jeffrey Snover said that PowerShell is finished, I died a little inside. Fortunately I was better prepared this time around when Angel said the same 🙂 . PowerShell has gained critical mass. Moving forward, PowerShell Core is where all attention will be placed. Steve Lee had some interesting charts and number he pulled from GitHub (It’s all about data). Mark Kraus is the nr 1 contributor on GitHub! Michael Greene’s go-to PowerShell version is Core! Joey even had a demo on cross-platform administration starting a session on a Windows and Linux. Unfortunately Joey forget about the Timezone difference, so his servers were down.

Keynote conclusion

Conclusion

Pretty much sums it up, no need to reiterate… 🙂

Networking event

Conferences are a great way  to interact with the Microsoft Team and delegates. It was awesome meeting Michael Greene and Steve lee in the flesh! I decided on a different approach when it came to mingling. I wanted to connect on a personal level. I can always reach out when I’m working on something. So here I am sitting in an Irish pub in Singapore knocking over a few with Michael, Amanda, Jason and Max! I asked Michael  what his thoughts were on running Windows for Workgroup 3.11 in Azure… Hypothetically that is :-P. What followed was a lively discussion! Jason had some real great stories to tell! Max told us about his first job in IT back in ’78 when debugging was literally ‘debugging’. Amanda was like: “I wasn’t even born yet…”

Benjamin had Joey finish his demo in the pub. Joey’s demo didn’t go through because of timezone difference (His Azure Servers shutdown automatically after-hours), but Benjamin wasn’t haven’t that. Joey complied and did the demo in the pub, with all attending cheering him on like proper hooligans! For outsiders we may as well have been watching a football game.

I even have my own fan club!

Suresh

Suresh follows my blog and was pretty excited to meet me! Lil’ ol’ me! Suresh made the 12 hour flight worth it! I enjoy meeting my PowerShell heroes in the flesh, I just never considered myself special… Thanks for the support!!! Appreciate it!

Slides & Code

Before I forget here’s the GitHub link to all my presentation slides and code. So Fabian Dibot was at my Infrastructure session and I almost didn’t recognize him! Both him and Mathias Jessen had fun sending twitter messages during my session 😛 . Tip: make sure you disable browser notification before starting you presentation… I know, a rookie mistake, you caught me guys… 😛

Bartek Bielawski was at my Dependencies session! That was a big honor for me! Bartek is the guy you go to when you’re really stuck! While doing my session I was trying to get a read on Bartek’s facial expression. At some point I could see him scanning the code… was that a smile? Ah! no comment whew! I couldn’t wait to ask him for feedback. Getting a compliment from Bartek on presentation preparation  & code definitely gave me a confidence boost! IMHO I enjoyed presenting this session the most. I did this as a flash session at one of our DuPSUG gatherings. I took a different route using AzureAD instead of plain ol’ AD and came across some fun stuff. AD and AzureAD have different parametersets 😉

I’m really glad I did the PSConf.asia sessions. Milton Goh was hinting on a surprise next year? 🙂 If I’m fortunate to be asked next year, I won’t hesitate!

Thanks for a wonderful experience PSConf.asia! I can’t wait to see where your Digital Transformation takes you!

PSConfAsia

Ttyl,

Urv

 

 

Microsoft Teams cmdlets are here!

‘Sup PSHomies,

Microsoft Teams has released the long anticipated Teams module! And with that a great blog to get you started.

Microsoft Teams is Microsoft’s version of Slack (Ok, I oversimplified that… I know). I’ve been following MSTeams development with Graph Explorer for some time now (Something you should definitely look into).

So I followed the blog’s instructions and everything works as expected!

The cmdlets support the pipeline.  (Get-Command -Name <cmdlet>).Parameterset will get you an overview of what’s supported and/or mandatory. Generallyspeaking, GroupId is mandatory and accepted from the pipeline.

TeamChannel supports value from pipeline

Here’s some code to help test drive the cmdlets.

#region Connect
Connect-MicrosoftTeams
#endregion
#region MSTeams cmdlets
#Get all cmdlets
Get-Command -Module *Teams*
#Only interested in Get-*?
Get-Command -Module *Teams* -Name Get*
#Find out what mandatory and/or accepts ValueFrom* for Get-TeamUser
(Get-Command -Name Get-TeamUser).ParameterSets
#endregion
#region Test drive cmdlets
#Get Team Displays all Teams (Ofcourse it does)
Get-Team
#Get Team Channels. GroupId ValueFromPipelineByPropertyName = True
Get-Team | Get-TeamChannel
#Group TeamChannels by Teams
Get-Team |
ForEach-Object{
$result = Get-TeamChannel -GroupId $_.GroupId
[PSCustomObject]@{
Team = $_.DisplayName
TeamChannel = $result
}
} -OutVariable TeamChannels
#Get Team Users. GroupId ValueFromPipelineByPropertyName = True
Get-Team | Get-TeamUser
#Get TeamUsers
Get-Team |
ForEach-Object{
$result = Get-TeamUser -GroupId $_.GroupId
[PSCustomObject]@{
Team = $_.DisplayName
Users = $result
}
} -OutVariable TeamUsers
#Get TeamMemberSettings
Get-Team |
ForEach-Object{
$result = Get-TeamMemberSettings -GroupId $_.GroupId
[PSCustomObject]@{
Team = $_.DisplayName
TeamMemberSettings = $result
}
} -OutVariable TeamMemberSettings
#endregion

So one thing I was hoping for, was to have a cmdlet to post to TeamChannels. For now posting can be done using Graph beta support for teams. Looking forward to having cmdlets for posting as well (fingers crossed!). Before the cmdlets you needed to do some pre-configuration in order to post using Graph, so the cmdlets definitely makes that step easier!

Well that’s it in a nutshell, happy Testing

Hope it’s worth something to you,

Ttyl,

Urv

 

Graph, the endpoint to end all endpoints

‘Sup PSHomies,

My first introduction to Graph was at the Microsoft Technical Summit here in the Netherlands at the RAI march 23,24 this year. It was actually the last session of the day. The intended audience was for developers… I thought to myself, why not? I might pick up a thing or two… It blew my mind!

Then at the PSConfEU at Hannover, Jeffrey Snover’s Keynote ‘State of Union’ really made me reassess my career, so much so that I left my previous employment of 23 years for a much smaller company, Methos! Methos is owned by Jeff Wouters aka “The scripting dutchman”. It’s like Jeffrey Snover said in his keynote: “With transformative change, you need to change the way you think about things.” Graph is one of those transformative thing… Here’s how I got started.

So Jeff came up with an idea:

Jeff : Say Urv, I got something I’d like you to look into…

Me: Sure, what’s up?

Jeff: Would it be possible to get a hierarchy of the entire company from Azure Active Directory?

Me: I don’t see why not?

Jeff: Once you have the data the next step is to visualize it!

Me: Like an organizational chart?

Jeff: Uhuh! (Grinning) Here’s a tip, look into Graph…

I should have known… 🙂 Now at Methos we don’t have an on-premise Active Directory so Azure AD it is. Now as part of my “transformative change” I asked for a Macbook pro. I thought: “Hey if you’re gonna go through the change (wait that doesn’t sound right, you know what I mean) might as well go big or go home! Jeff didn’t disagree, he only grinned and let out a subtle evil laugh… I got a sneaky suspicion that I’m in for a surprise… My other Transformative change: PowerShell Core!

I installed Azure CLI 2.0 as a first attempt. To get Azure AD users:

az ad user list

This gave me a JSON list with few attributes. Maybe I need to add a property list or something. Let do a quick help, Hmmm… Ok… Next approach.

Let’s install AADPreview Core. Say, Get-AzureADUser doesn’t have a -Properties attribute… Huh? So that’s why Jeff was grinning about… Well played Jeff…

So here’s the deal, I’m using PowerShell core, not all modules are applicable. The good thing is that I could login to Graph Explorer.  Graph explorer gives you the opportunity to explore the endpoint and browse around.

“Hey look at that, there’s a get manager query! Maybe there’s a way to get all the data and filter out what I need (Like I always do with PSCustomObject)”

I saw one of the examples use a $select option so I decided to give it a try.

$select=displayName, GivenName, surName, department, officeLocation, jobTitle, userPrincipalName, id, manager

I decided to give it a try. Ok that narrowed things down a bit still no manager in sight! Turns out manager is a special kind of property, a Navigation Property. You can extract it from the MetaData

$graphMetadata = [XML](Invoke-WebRequest -Uri 'https://graph.microsoft.com/v1.0/$metadata').Content
$userMetaData = $graphMetadata.Edmx.DataServices.Schema.EntityType.Where{ $_.Name -eq 'User' }
$userMetaData

Graph-NavigationProperty

Ah, that’s why manager is omitted, it isn’t part of the collection! My next challenge was to retrieve the JSON payload. That’s where I ran into the accessToken dilemma.

 

AccessToken

From the online resources I inspected, authentication is the key to access (see what I did there?). I found Mark Kraus’s module PSMSGraph quite useful to follow his trail of thoughts. It didn’t work for me because I’m using PowerShell core. Now it’s time to read the document! When all else fails RTFM!

I ran into some snags with my application registration, so my next step was to reach out to the Graph team! They are on twitter just no tweets… yet! That got me in contact with Dmitry Pimenov, the PM of Microsoft Graph! I explained him my dilemma and he showed me an easy way to get the accesstoken for proto-type purposes. Finally! I can get some data to play with!

#region Get Access token from Grap Explore Session
$accessToken = #Get the AccessTokenquick & Dirty using developers tool trick by Dan Silver https://twitter.com/dansilver82/status/872520061843415040
#endregion
#region Format request Header
$requestHeader = @{
"Authorization" = "Bearer " + $accessToken #For now get AccessToken from Developers tools console
"Content-Type" = "application/json"
}
#endregion
#region Get Users Tenant properties to query with Graph
$userProperties = @(
'displayName'
'GivenName'
'surName'
'department'
'officeLocation'
'jobTitle'
'userPrincipalName'
'id'
)
$iwrUsersParams = @{
Uri = "https://graph.microsoft.com/v1.0/users?`$select=$($userProperties -join ',')"
Method = 'Get'
Headers = $requestHeader
}
$iwrUsersResults = Invoke-WebRequest @iwrUsersParams
$iwrUsersObjects = $iwrUsersResults.Content | ConvertFrom-Json
$iwrUsersObjects.value
#endregion
#region Get Manager per Object ID
$iwrUsersObjects.value |
ForEach-Object {
try {
$obj = $_
$iwrManagerParams = @{
Uri = 'https://graph.microsoft.com/v1.0/users/{0}/manager' -f $obj.ID
Method = 'Get'
Headers = $requestHeader
}
$userManager = Invoke-WebRequest @iwrManagerParams
$iwrManagerObject = $userManager.Content | ConvertFrom-Json
[PSCustomObject]@{
ObjectID = $obj.Id
UserPrincipalName = $obj.userPrincipalName
DisplayName = $obj.DisplayName
GivenName = $obj.givenName
SurName = $obj.surname
JobTitle = $obj.jobTitle
Department = $obj.department
Manager = $iwrManagerObject.displayName
}
}
Catch {
[PSCustomObject]@{
ObjectID = $obj.Id
UserPrincipalName = $obj.userPrincipalName
DisplayName = $obj.DisplayName
GivenName = $obj.givenName
SurName = $obj.surname
JobTitle = $obj.jobTitle
Department = $obj.department
Manager = $null
}
}
}
#endregion
#region Get Access token from Grap Explore Session
$accessToken = $null
#endregion
#region Format request Header
$requestHeader = @{
"Authorization" = "Bearer " + $Accesstoken #For now get AccessToken from Developers tools console
"Content-Type" = "application/json"
}
#endregion
$iwrUsersParams = @{
Uri = "https://graph.microsoft.com/beta/users?`$expand=Manager"
Method = 'Get'
Headers = $requestHeader
}
$iwmUserResults = Invoke-RestMethod @iwrUsersParams
$iwmUserResults.value |
ForEach-Object {
[PSCustomObject]@{
DisplayName = $_.displayname
GivenName = $_.givenname
SurName = $_.surname
JobTitle = $_.jobtitle
Department = $_.department
Manager = $_.manager.displayname
}
}

My first attempt was straight forward: get the user, get the manger for each user and then create a PSCustomObject. This isn’t optimal ofcourse. So I sent Dmitry a DM asking what is the optimal way of retrieving manager in an ODATA query. That’s when he explained that the Beta version supports the $expand option in the ODATA query.

Quick side step: Microsoft Graph supports ODATA queries, not all queries are supported yet, it’s a work in progress. ODATA is definitely something to look into… Here’s a link to get you started… Transformative change… 😉

Alright, so the /Beta returned  quite a bit more than I bargained for, but at least I didn’t have to do multiple queries. That’s when I decided to trying something new… JMESPath Query!

JMESPath

While using az I noticed a link to JMESpath query, what’s that?  JMESPath is a query language for JSON. With it, you can extract and transform elements from a JSON document… Sweet! Kinda like what we do with PSCustomObject, just on JSON. Here’s where having a Mac wasn’t a liability after all… I even found a vscode extension for jmespath queries! Many ways to Rome… Transformative change… (Jeffrey Snover was right, you need to rethink the way you think about things)

Using this query on the JSON document gave me the following results:

value[].{DisplayName: displayName, GivenName: givenName, SurName: surname, Department: department, JobTitle: jobTitle, Manager:manager.displayName, Location:officeLocation}

JSOn-ManagerResults

Alright! Prototyping mission accomplished! What I thought was straight forward had quite a few curves, but I wouldn’t have it any other way!

Take away:

Learn to love Graph! Be open to learning new and interesting way to accomplish tasks in a new way. Transformative change  won’t be easy, but it’ll be worth it!!!

Hope it’s worth something to you…

Ttyl,

Urv

PSDay.uk Slides & Code

I had the pleasure of presenting at PSDay.uk recently. PSDay uk was sponsored by Ticketmaster and G-Research in London. I presented the following topic:

PowerShell Classes for Ops transitioning to Dev

I’ve been fascinated by PowerShell classes since its introduction in v5. When Rob Sewell ask me to do a session at the PSDay.uk, I didn’t have to think too long about the topic 🙂

I’ll be honest that the first 5 minutes was kinda nerve wrecking. I wasn’t nervous at first until I stood there and had some kind of outer-body experience…

Holy crap this is really happening!!! Calm down Urv… Whooza… Get to the code…

Once I got settled in, my nerves calmed down…

The code is bases on an interaction I had with Jaap Brasser (The legend himself 😉 ) on his blog a while back. At first I thought I didn’t have enough code but it all timed-out perfectly in the end.

Here’s the link to the code & presentation.

Shout out to Doug Finke for taking the time to break down classes for me. Doug is a true Developer and an great guy! I truly believe that the right people are placed in our paths when we need them most. Yet only if we remain open-minded and willing to learn can these mentors help us reach new levels of development. I can’t thank you enough Doug, your contribution to the community is plain awesome! You make me want to be a better coder!

Thank you PSDay.uk for this great experience! Til’ next time? 😛

Ttyl,

Urv

 

Protecting output content from prying eyes

Sup PSHomies,

Security is on everyone’s mind these days. It’s no wonder, with GDPR  going in effect as of may 2018.  My quick take on GDPR? It’s just Data Protection & ILM (Information Life-cycle Management) with some serious penalty consequences.  That archiving solution isn’t looking that expensive anymore eh? Having a RBAC model in place makes a whole lot of sense right about now huh? But I digress…

I get asked a lot to create reports. GDPR made me stop and reflect on how well (or maybe not so well) I’m handling/sharing these reports. Let’s just say I didn’t give myself a high score…

So my next challenge is about security, how do I keep prying eyes off my data? How do I make sure only the intended audience has access to the data?

First thing that came to mind was encryption. Come to find out that PowerShell v5 has some new cmdlets, Protect/Unprotect-CMSMessage,for this very purpose! Keith Hill has an excellent blog about getting started with them (seriously how did I miss this???). Oh and be sure to read Dave Wyatt’s comment.

Another must read blog is that of Cyber Defense. I tried the code and was able  to recreate the limitations, errors and performance issues. I also tried exporting to XML using Export-CliXML, but that gave me some issues with UTF8 encoding.  I was able to protect a 500 MB file, but unable to unprotect it. Found yet another great tip on Compressing files using the 7zip Module (Could it get any better???) The cmdlets are great for encrypting simple text.

Best route for protecting your data is to encrypt your password and use that password to protect your data as a zip file. Here’s what I came up with…

<#
Author: I. Strachan
Version: 1.0
Version History:
Purpose: Create an encrypted password file and saves the password as
a Credential object
#>
function New-EncryptedCredentialFile {
[CmdletBinding()]
param(
[String]$ThumbPrint = '0460483ADEF613D3B1781FAE393DF2AEAE1060ED',
[String]$PasswordFile = 'C:\scripts\source\txt\Password.txt',
[ValidateRange(20, 128)]
[Int]$PasswordLength = 100
)
#Load "System.Web" assembly in PowerShell console
$null = [Reflection.Assembly]::LoadWithPartialName("System.Web")
#Calling Generated Password Method
$ClearPassword = [System.Web.Security.Membership]::GeneratePassword($PasswordLength, 0)
$ClearPassword |
Protect-CmsMessage -OutFile $PasswordFile -To $ThumbPrint
#return Credential for encryption
$Credential = [PSCredential]::new($ThumbPrint, $($ClearPassword | ConvertTo-SecureString -AsPlainText -Force))
$Credential
}
$Cred = New-EncryptedCredentialFile -ThumbPrint '0A5A254C42D710E2C0B1BC77B142FAEC7EA7B93B'
#Region Compress and password protect 7zip file
$param7Zip = @{
Path = 'c:\scripts\export\dsa\20062017'
ArchiveFileName = 'pshirwin-20062017.7zp'
Format = 'SevenZip'
SecurePassword = $Cred.Password
}
Compress-7Zip @param7Zip
#Endregion
#Region Expand 7Zip protected password file
#Retrieve exported Credentials
$thumbPrint = '0A5A254C42D710E2C0B1BC77B142FAEC7EA7B93B'
$passwd = Unprotect-CmsMessage -LiteralPath 'C:\scripts\sources\txt\Password.txt' -To $thumbPrint |
ConvertTo-SecureString -AsPlainText -Force
$Creds = [PSCredential]::new($thumbPrint, $passwd)
Expand-7Zip -ArchiveFileName 'pshirwin-20062017.7zp' -TargetPath .\temp\pshirwin-20062017 -SecurePassword $Creds.Password
#Endregion

Let’s break down the function. First you’re going to need the public key of the intended party. Keith’s blog covers how to do that. The function will generate a 100 random char password, encrypt it and save it to specified file. Last but not least it will also return said Password as a Credential object. This will be used later on to password protect the zip file.

This will at least protect you data from prying eyes. Like Dave said in his comment:

“Anyone with admin access to a computer, or physical access to an unencrypted hard drive, can steal those private keys in most cases.”

It’s not fool-proof but it’s a start…

Take away

Make sure you’re not the weakest link when it comes to security and protecting/sharing data. Make sure you understand your company’s GDPR compliance policies. I’m seeing quite a few Security officer job offers on LinkedIn if that’s your thing… 😉

One more blog I can definitely recommend reading is David das Neves blog on Powershell Security at Enterprise customers . Granted it’s quite the read but well worth it!

Ok, there’s just one more blog you should also read, Don Jones’ blog on stop using self-signed certificates

It’s going to be a challenge for all of us, but one well worth it…

Hope it’s worth something to you…

Ttyl,

Urv