Scripting Azure VM build tasks: static IP addresses, BGInfo and anti-malware extensions

Following on from yesterday’s blog post with a pile of PowerShell to build a multiple-NIC VM in Azure, here are some more snippets of PowerShell to carry out a few build-related activities.

Setting a static IP address on a NIC

$RGName = Read-Host "Resource Group"
$VNICName = Read-Host "vNIC Name"
$VNIC=Get-AzureRmNetworkInterface -Name $VNICName -ResourceGroupName $RGName
$VNIC.IpConfigurations[0].PrivateIpAllocationMethod = "Static"
Set-AzureRmNetworkInterface -NetworkInterface $VNIC

Installing BGInfo

$RGName = Read-Host "Resource Group"
$VMName = Read-Host "Virtual Machine Name"
$Location = Read-Host "Region/Location"
Set-AzureRmVMExtension -ExtensionName BGInfo -Publisher Microsoft.Compute -Version 2.1 -ExtensionType BGInfo -Location $Location -ResourceGroupName $RGName -VMName $VMName

Installing Microsoft Antimalware

This one is a little more difficult – the script is a development of Mitesh Chauhan’s work entitled Installing Microsoft Anti Virus Extension to Azure Resource Manager VM using Set-AzureRmVMExtension

It’s worth reading Mitesh’s post for more background on the Microsoft Anti Virus Extension (IaaS Antimalware) and also taking a look at the Security Health in the Azure Portal (currently in preview), which will highlight VMs that have no protection (amongst other things).

Mitesh’s script uses a simple settings string, or for more complex configuration, it reads from a file. I tried to use a more complex setting and it just resulted in PowerShell errors, suggesting this wasn’t proper JSON (it isn’t):

$AntiMalwareSettings = @{
"AntimalwareEnabled" = $true;
"RealtimeProtectionEnabled" = $true;
"ScheduledScanSettings" = @{
"isEnabled" = $true;
"day" = 1;
"time" = 180;
"scanType" = "Full"
};
"Exclusions" = @{
"Extensions" = ".mdf;.ldf;.ndf;.bak;.trn;";
"Paths" = "D:\\Logs;E:\\Databases;C:\\Program Files\\Microsoft SQL Server\\MSSQL\\FTDATA";
"Processes" = "SQLServr.exe;ReportingServicesService.exe;MSMDSrv.exe"
}
}

Set-AzureRmVMExtension : Error reading JObject from JsonReader. Current JsonReader item is not an object: Null. Path”, line 1, position 4.

If I use the JSON form it’s no better:

$AntiMalwareSettings = {
"AntimalwareEnabled": true,
"RealtimeProtectionEnabled": true,
"ScheduledScanSettings": {
"isEnabled": true,
"day": 1,
"time": 180,
"scanType": "Full"
},
"Exclusions": {
"Extensions": ".mdf;.ldf;.ndf;.bak;.trn",
"Paths": "D:\\Logs;E:\\Databases;C:\\Program Files\\Microsoft SQL Server\\MSSQL\\FTDATA",
"Processes": "SQLServr.exe;ReportingServicesService.exe;MSMDSrv.exe"
}
}

Set-AzureRmVMExtension : Unexpected character encountered while parsing value: S. Path ”, line 0, position 0.

So the actual script I used is below:

# Install Microsoft AntiMalware client on an ARM based Azure VM
# Check note at the end to be able to open up the SCEP antimalware console on the server if there are problems.
# Author – Mitesh Chauhan – miteshc.wordpress.com (updated by Mark Wilson - markwilson.co.uk)
# For Powershell 1.0.1 and above
# See https://miteshc.wordpress.com/2016/02/18/msav-extension-on-azurearm-vm/

# Log in with credentials for subscription
# Login-AzureRmAccount

# Select your subscription if required (or default will be used)
# Select-AzureRmSubscription -SubscriptionId "Your Sub ID here"

$RGName = Read-Host "Resource Group"
$VMName = Read-Host "Virtual Machine Name"
$Location = Read-Host "Region/Location"

# Use this (-SettingString) for simple setup
# $AntiMalwareSettings = ‘{ "AntimalwareEnabled": true,"RealtimeProtectionEnabled": true}’;

# Use this (-SettingString) to configure from JSON file
$AntiMalwareSettings = Get-Content ‘.\MSAVConfig.json’ -Raw

$allVersions= (Get-AzureRmVMExtensionImage -Location $location -PublisherName "Microsoft.Azure.Security" -Type "IaaSAntimalware").Version
$typeHandlerVer = $allVersions[($allVersions.count)–1]
$typeHandlerVerMjandMn = $typeHandlerVer.split(".")
$typeHandlerVerMjandMn = $typeHandlerVerMjandMn[0] + "." + $typeHandlerVerMjandMn[1]

Write-Host "Installing Microsoft AntiMalware version" $typeHandlerVerMjandMn "to" $vmName "in" $RGName "("$location ")"
Write-Host "Configuration:"
$AntiMalwareSettings

# Specify for -SettingString parameter here which option you want, simple $settingsstring or $MSAVConfigfile to sue json file.
Set-AzureRmVMExtension -ResourceGroupName $RGName -VMName $vmName -Name "IaaSAntimalware" -Publisher "Microsoft.Azure.Security" -ExtensionType "IaaSAntimalware" -TypeHandlerVersion $typeHandlerVerMjandMn -SettingString $AntiMalwareSettings -Location $location

# To remove the AntiMalware extension
# Remove-AzureRmVMExtension -ResourceGroupName $resourceGroupName -VMName $vmName -Name "IaaSAntimalware"

# If you have error saying Admin has restricted this app, Navigate to “C:\Program Files\Microsoft Security Client”
# Run "C:\Program Files\Microsoft Security Client\ConfigSecurityPolicy.exe cleanuppolicy.xml"
# Or simply drag the cleanuppolicy.xml file above onto the ConfigSecurityPolicy.exe to sort it and you should be in.

The MSAVconfig.json file contains the JSON version of the Anti-Malware settings:

{
"AntimalwareEnabled": true,
"RealtimeProtectionEnabled": true,
"ScheduledScanSettings": {
"isEnabled": true,
"day": 1,
"time": 180,
"scanType": "Full"
},
"Exclusions": {
"Extensions": ".mdf;.ldf;.ndf;.bak;.trn",
"Paths": "D:\\Logs;E:\\Databases;C:\\Program Files\\Microsoft SQL Server\\MSSQL\\FTDATA",
"Processes": "SQLServr.exe;ReportingServicesService.exe;MSMDSrv.exe"
}
}

Building a multiple NIC VM in Azure

I recently found myself in the situation where I wanted to build a virtual machine in Microsoft Azure (Resource Manager) with multiple network interface cards (vNICs). This isn’t available from the portal, but it is possible from the command line.

My colleague Leo D’Arcy pointed me to Samir Farhat’s blog post on how to create a multiple NIC Azure virtual machine (ARM). Samir has posted his script on the TechNet Gallery but I made a few tweaks in my version:

#Variables
$VMName = Read-Host "Virtual Machine Name"
$RGName = Read-Host "Resource Group where to deploy the VM"
$Region = Read-Host "Region/Location"
$SAName = Read-Host "Storage Account Name"
$VMSize = Read-Host "Virtual Machine Size"
$AvailabilitySet = Read-Host "Availability Set ID (use Get-AzureRMAvailabilitySet to find this)"
$VNETName = Read-Host "Virtual Network Name"
$Subnet01Name = Read-Host "Subnet 01 Name"
$Subnet02Name = Read-Host "Subnet 02 Name"
$cred=Get-Credential -Message "Name and password for the local Administrator account"
 
# Getting the Network
$VNET = Get-AzureRMvirtualNetwork | where {$_.Name -eq $VNETName}
$SUBNET01 = Get-AzureRmVirtualNetworkSubnetConfig -Name $Subnet01Name -VirtualNetwork $VNET
$SUBNET02 = Get-AzureRmVirtualNetworkSubnetConfig -Name $Subnet02Name -VirtualNetwork $VNET
 
# Create the NICs
$NIC01Name = $VMName+'-NIC-01'
$NIC02Name = $VMName+'-NIC-02'
Write-Host "Creating" $NIC01Name
$VNIC01 = New-AzureRmNetworkInterface -Name $NIC01Name -ResourceGroupName $RGName -Location $Region -SubnetId $SUBNET01.Id
Write-Host "Creating" $NIC02Name
$VNIC02 = New-AzureRmNetworkInterface -Name $NIC02Name -ResourceGroupName $RGName -Location $Region -SubnetId $SUBNET02.Id
 
# Create the VM config
Write-Host "Creating the VM Configuration"
$VM = New-AzureRmVMConfig -VMName $VMName -VMSize $VMSize -AvailabilitySetId $AvailabilitySet
$pubName="MicrosoftWindowsServer"
$offerName="WindowsServer"
$skuName="2012-R2-Datacenter"
Write-Host " - Setting the operating system"
$VM = Set-AzureRmVMOperatingSystem -VM $vm -Windows -ComputerName $vmName -Credential $cred -ProvisionVMAgent -EnableAutoUpdate
Write-Host " - Setting the source image"
$VM = Set-AzureRmVMSourceImage -VM $vm -PublisherName $pubName -Offer $offerName -Skus $skuName -Version "latest"
#Adding the VNICs to the config, you should always choose a Primary NIC
Write-Host " - Adding vNIC 1"
$VM = Add-AzureRmVMNetworkInterface -VM $VM -Id $VNIC01.Id -Primary
Write-Host " - Adding vNIC 2"
$VM = Add-AzureRmVMNetworkInterface -VM $VM -Id $VNIC02.Id
 
# Specify the OS disk name and create the VM
$DiskName=$VMName+'-OSDisk'
Write-Host " - Getting the storage account details"
$SA = Get-AzureRmStorageAccount | where { $_.StorageAccountName -eq $SAName}
$OSDiskUri = $SA.PrimaryEndpoints.Blob.ToString() + "vhds/" + $vmName+"-OSDisk.vhd"
Write-Host " - Setting up the OS disk"
$VM = Set-AzureRmVMOSDisk -VM $VM -Name $DiskName -VhdUri $osDiskUri -CreateOption fromImage
Write-Host "Creating the virtual machine"
New-AzureRmVM -ResourceGroupName $RGName -Location $Region -VM $VM

Short takes: deleting bit.ly Bitlinks; backing up and restoring Sticky Notes; accessing cmdlets after installing Azure PowerShell

Another collection of short notes to add to my digital memory…

Deleting bit.ly links

Every now and again, I spot some spam links in my Twitter feed – usually prefixed [delicious]. That suggests to me that there is an issue in Delicious or in Twitterfeed (the increasingly unreliable service I use to read certain RSS feeds and tweet on my behalf) and, despite password resets (passwords are so insecure) it still happens.

A few days ago I spotted some of these spam links still in my bit.ly links (the link shortener behind my mwil.it links, who also own Twitterfeed) and I wanted to permanently remove them.

Unfortunately, according to the “how do I delete a Bitlink” bit.ly knowledge base article – you can’t.

Where does Windows store Sticky Notes?

Last Friday (the 13th) I wrote about saving my work before my PC was rebuilt

One thing I forgot about was the plethora of Sticky Notes on my desktop so, today, I was searching for advice on where to find them (in my backup) so I could restore.

It turns out that Sticky Notes are stored in user profiles, under %appdata%\Microsoft\Sticky Notes, in a file called StickyNotes.snt. Be aware though, that the folder is not created until the Sticky Notes application has been run at least once. Restoring my old notes was as easy as:

  1. Run the Sticky Notes desktop application in Windows.
  2. Close Sticky Notes.
  3. Overwrite the StickyNotes.snt file with a previous copy.
  4. Re-open Sticky Notes.

Azure PowerShell installation requires a restart (or explicit loading of modules)

This week has involved a fair amount of restoring tools/settings to a rebuilt PC (did I mention that mine died in a heap last Friday? If only the hardware and software were supplied by the same vendor – oh they are!). After installing the Azure PowerShell package from the SCCM Software Center, I found that cmdlets returned errors like:

Get-AzureRmResource : The term ‘Get-AzureRmResource’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

After some RTFMing, I found this:

This can be corrected by restarting the machine or importing the cmdlets from C:\Program Files\WindowsPowerShell\Modules\Azure\XXXX\ as following (where XXXX is the version of PowerShell installed[)]: import-module "C:\Program Files\WindowsPowerShell\Modules\Azure\XXXX\azure.psd1" import-module "C:\Program Files\WindowsPowerShell\Modules\Azure\XXXX\expressroute\expressroute.psd1"

PowerShell snippets

Writing PowerShell scripts over the last couple of weeks has been a steep curve. I’ve watched PowerShell from afar over the years but don’t really use it enough to say I know it.  Luckily, many others do, and they’ve posted their knowledge on the ‘net. This is what I drew upon:

Unable to set the default PowerShell font to Lucida Console

Windows PowerShell with a tiny raster fontFor the last few months, I’ve been getting more and more infuriated with my PowerShell sessions opening in a tiny raster font (4×6). On a high resolution display like the one on the Surface Pro 3, that’s a complete pain and, whilst I could change the font in the properties for that session, it wasn’t “sticky”, with an error that said:

Windows PowerShell - error updating shortcutError Updating Shortcut

Unable to modify the shortcut: Check to make sure it has not been deleted or renamed.

For reference, I’m experiencing this on Windows 8.1, 64-bit and it only applies to the Windows PowerShell shortcut and the Microsoft Azure PowerShell shortcut – not to the PowerShell ISE, nor to the various shortcuts created by modules like the SharePoint Online Management Shell, Lync Server Management Shell or the Windows Azure Active Directory Module for Windows PowerShell.

Solving the “stickiness” of my changes was simple enough – I asked our support team to change the permissions on the shortcut to allow Users to Modify it – but I still couldn’t get it to stay on my preferred setting: Lucida Console 20.

I could set it to Consolas, or raster fonts (urgh), but Lucida Console just wouldn’t stick. It’s been recorded as a bug in Microsoft Connect for a couple of years but there’s no sign of a fix yet (not even in Windows 10).

Being unable to set the default PowerShell font to Lucida Console seems to be a widely recognised problem. Various options are discussed on this SuperUser post including that it may be a language issue. Others have suggested the issue is the space in the font name, with a workaround that involves installing a new font and editing the registry (not an option for me without administrator permissions). I also looked at using the SetConsoleFont module to change the font within my PowerShell profile but struggled to work out the settings I would require.

In the end, I gave up and accepted that Consolas 24 is vastly preferable to a 4×6 raster font!

Announcing posh-o365, a collection of PowerShell scripts for Office 365 management

I have a growing collection of PowerShell scripts for Office 365 management and, whilst I may not be the best scripter/coder out there (I’m pretty sure there are many people who could look at my scripts and say “there’s a better way to do that”), I’m open-sourcing them in the hope that others will contribute.

The current scripts are:

  • Connect-O365.ps1 – connect to multiple Office 365 sessions in a single PowerShell window.
  • Check-ODBQuotas.ps1 – checking a list of UPNs to see if their OneDrive for Business folders have been created and, if so, if their quotas need to be set.
  • Set-O365Licences.ps1 – setting usage locations and applying/removing licences for Office 365 users.
  • Set-ODBQuota.ps1 – setting a quota for a user’s OneDrive for Business folder.

More details on the use of these scripts can be found in the readme file for the project.

Others scripts are in development for:

  • Setting restrictions on audio/video settings in Skype for Business Online.

This collection is up on github under the name of posh-o365 and I’d be interested to hear what people think of these, and what other scripts might be useful for performing Office 365 management tasks at scale in an enterprise context.

Posh-git!

I’ve been writing quite a lot of PowerShell recently and I decided that I really should look into using some kind of source control system.  I’ve used Git before, integrating GitHub with VisualStudio and this time I wanted to integrate with the PowerShell ISE (by the way, if you don’t have a Windows 8.1 shortcut for the PowerShell ISE, Carlo/Happy SysAdmin describes how to find it).

It’s actually pretty straightforward using Keith Dahlby’s posh-git scripts (what a great name that is!) and GitHub Desktop. Jeff Yates has a post that describes the process but I found I needed to do a bit more to make it work for me…

My PowerShell ISE didn’t have a profile, so I created one and added the entries Jeff suggests but, even so, I was getting errors in my console:

Resolve-Path : Cannot find path ‘C:\Users\username\AppData\Local\GitHub\PoshGit_869d4c5159797755bc04749db47b166136e59132’ because it does not exist.
At C:\Users\username\AppData\Local\GitHub\shell.ps1:26 char:26
+ $env:github_posh_git = Resolve-Path “$env:LocalAppData\GitHub\PoshGit_869d4c51 …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Users\username\…47b166136e59132:String) [Resolve-Path], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.ResolvePathCommand

Resolve-Path : Cannot find path ‘C:\profile.example.ps1’ because it does not exist.
At C:\users\username\Documents\WindowsPowerShell\Microsoft.PowerShellISE_profile.ps1:3 char:4
+ . (Resolve-Path “$env:github_posh_git\profile.example.ps1”)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\profile.example.ps1:String) [Resolve-Path], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.ResolvePathCommand

The expression after ‘.’ in a pipeline element produced an object that was not valid. It must result in a command name, a script block, or a CommandInfo object.
At C:\users\username\Documents\WindowsPowerShell\Microsoft.PowerShellISE_profile.ps1:3 char:3
+ . (Resolve-Path “$env:github_posh_git\profile.example.ps1”)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : BadExpression

After some hunting around, and an attempt to install posh-git manually, I realised that the shell.ps1 script from Github Desktop was looking for posh-git in a folder at $env:LocalAppData\GitHub called PoshGit_869d4c5159797755bc04749db47b166136e59132 (I’m guessing that’s a GUID in the foldername, so it’s likely to be different for others). After copying a clone of posh-git to that folder and reloading my profile (. $profile), everything was working as it should to commit changes from within PowerShell.

Short takes: checking your IP in Google; writing to a text file in PowerShell; and confirming which IE security zone a website uses in Internet Explorer

Another eclectic mix of snippets merged into a single blog post…

What’s my IP address?

Ever want to check the IP address of the connection you’re using? There are lots of websites out there that will tell you, or you can just type what is my IP into Google (other search engines are available… but they won’t directly return this information).

Writing output to a text file in PowerShell

Sometimes, when working in PowerShell, it’s useful to pipe the output to a file, for example to send to someone else for analysis. For this, the Out-File cmdlet comes in useful (| Out-File filename.txt) , as described on StackOverflow.

Internet Explorer status bar no longer shows security zone for a site

Last week, I was trying to work out which security zone a site was in last week (because I wanted to see if it was in the Intranet zone, whilst tracking down some spurious authentication prompts) but recent versions of Internet Explorer don’t show this information in the status bar. The workaround is to right click any black space in the website and select Properties. Alternatively, use Alt + F + R.

Check the security zone in Internet Explorer

Office 365 command line administration (redux)

Every now and again, I find myself looking up the same things for Office 365 command line administration (i.e. using PowerShell), so it’s probably worth me writing them down in one post…

Of course, a connection to Office 365 from PowerShell is a pre-requisite – although that’s a lot simpler now than it used to be as there’s no longer any need for the Microsoft Online Services Sign In Assistant (MOS SIA), just:

Import-Module MSOnline
$Credential = Get-Credential
Connect-MsolService -credential $Credential

If you’re doing this in a script, you might want to save the password as a secure string (as described in more detail by Kris Powell):

(Get-Credential).Password | ConvertFrom-SecureString | Out-File Password.txt

To use the secure string:

$User = "alias@domainname.tld"
$Pass = Get-Content "Password.txt" | ConvertTo-SecureString
$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $user,$pass

Then Connect-MsolService -credential $Credential as above.

Setting a user password (and making sure you don’t need to force a change – one reason to do it from PowerShell rather than the web portal) involves:

Set-MsolUserPassword -UserPrincipalName alias@domainname.tld -forcechangepassword $false -newpassword password

And, if it’s a service account, turn off password expiry?

Set-MsolUser -UserPrincipalName alias@domainname.tld -PasswordNeverExpires $true

 

Getting to grips with Office 365 Message Encryption

As part of my work this week with Exchange transport rules, I needed to recreate another facility that my customer has grown used to in Office 365 – the ability to selectively encrypt emails using keywords.

This one turned out to be relatively straightforward – Office 365 Message Encryption has been around for a while now (it replaced Exchange Hosted Encryption) and I was able to use a transport rule to detect a phrase in the subject or body (“encrypt me please”) and apply Office 365 Message Encryption accordingly. I could equally have done this based on other criteria (for example, I suggest that any message marked as confidential and sent externally would be a good candidate).

So, the rule is fairly simple:

New-TransportRule -Name 'Encrypt email on request' -Comments ' ' -Mode Enforce -SubjectOrBodyContainsWords 'encrypt me please' -ApplyOME $true

Office 365 Message Encryption needs Azure RMS

The challenge for me was that I wasn’t creating it in PowerShell – I was using the Exchange Admin Center and the appropriate options weren’t visible. That’s because Office 365 Message Encryption needs Azure Rights Management Services (RMS) to be enabled, and it’s necessary to use the More Options link to expose the option to Modify the Message Security… from which it’s possible to Apply Office 365 Message Encryption.

Unfortunately that still didn’t work and the resulting error message was:

You can’t create a rule containing the ApplyOME or RemoveOME action because IRM licensing is disabled.

It seems it’s not just a case of enabling RMS in the service settings. I also needed to run the following commands in PowerShell:

Set-IRMConfiguration –RMSOnlineKeySharingLocation “https://sp-rms.eu.aadrm.com/TenantManagement/ServicePartner.svc”

(that’s the European command – there are alternative locations for other regions listed in the post I used to help me)

Import-RMSTrustedPublishingDomain -RMSOnline -name "RMS Online"
Test-IRMConfiguration -RMSOnline

(check everything passes)

Set-IRMConfiguration -InternalLicensingEnabled $true

With RMS/Information Rights Management (IRM) properly enabled I could create the rule as intended.

Customising the experience

Testing my rule was easy enough, but it’s also possible to customise the portal that recipients go to in order to read the encrypted message.

This is all done in PowerShell, with some simple commands:

Get-OMEConfiguration provides the current Office 365 Message Encryption configuration and to set the configuration to meet my requirements, I used:

Set-OMEConfiguration -Identity "OME Configuration" -Image (Get-Content "markwilsonitlogo.png" -Encoding byte) -PortalText "markwilson.it Secure Email Portal" -EmailText "Encrypted message from markwilson.it"

The tricky bit was working out how to provide the logo file as just the filename creates a PowerShell error and the Get-Content cmdlet has to be used to encode the file.

Further reading

Office 365 Message Encryption (and decryption) – steps – understanding, purchase options, configuration, branding and use.