Script to import Office 365 IP list to Cisco ASA – Improved via REST API

Due to replacement of the Office 365 IP list with new web service, I wrote a new, improved script that returns all IPs needed for all services of O365 in format for Cisco ASA:

# CHANGE PATH
$Path = 'E:'
 
$Report = "$Path\Report.txt"
$date = Get-Date -format "yyyyMMdd"
$dateReport = Get-Date -format "dd.MM.yyyy"
$o365SavedGUID = "$Env:TEMP\o365_Guid.txt"
if (Test-Path $o365SavedGUID ) { $GUID = Get-Content $o365SavedGUID }
else { $GUID = [GUID]::NewGuid().Guid; $GUID | Out-File $o365SavedGUID }
[bool]$NoIpv6 = 1 # 1 eq True; 0 eq False
$Link = "https://endpoints.office.com/endpoints/Worldwide?NoIPv6=$NoIpv6&ClientRequestId=$GUID"
 
try { $AllIPs = Invoke-RestMethod -Method Get -Uri $Link }
catch  {
    $_ | Out-File "$Path\$date - Errors.txt"
    Add-Content $Report "$dateReport Error!!!"
}
 
if (!(Test-Path "$Path\$date - Errors.txt")) {
    $Result = @{}
    $IPGroupsTcp = $AllIPs | ? {($PSItem.Ips) -and ($PSItem.tcpPorts)} | select Ips, tcpPorts | Group-Object tcpPorts
    $IPGroupsUdp = $AllIPs | ? {($PSItem.Ips) -and ($PSItem.udpPorts)} | select Ips, udpPorts | Group-Object udpPorts
 
    foreach ($IPgrp in $IPGroupsTcp) {
        $ErrorList = @()
        $Result += @{"PortsTcp_$($IPgrp.Name)" = @()}
        $List = $IPgrp.Group.Ips | sort | select -Unique
        foreach ($Address in $List) {
            try {
                [string]$Parse = $Address
                [string]$IPAddress = $Parse.Substring(0,$Parse.IndexOf("/"))
                [string]$Cidr = $Parse.Substring($Parse.IndexOf("/")+1,$Parse.Length - $Parse.IndexOf("/")-1)
                [int]$Prefix = $Cidr
                if ($Prefix -eq 32) { [string]$Value = "network-object host $IPAddress" }
                else {
                    $mask = ([Math]::Pow(2,$Prefix)-1) * [Math]::Pow(2,(32-$Prefix))
                    $bytes = [BitConverter]::GetBytes([UInt32] $mask)
                    $IPMask = (($bytes.Count-1)..0 | ForEach-Object {[String]$bytes[$_]}) -join "."
                    [string]$Value = "network-object $IPAddress $IPMask"
                } # End if & else
                $Result."PortsTcp_$($IPgrp.Name)" += $Value.Trim()
            } # End Try
            catch { $ErrorList += "$Address --> $_" } # End Try & catch
        } # End foreach Address
    }
 
    foreach ($IPgrp in $IPGroupsUdp) {
        $ErrorList = @()
        $Result += @{"PortsUdp_$($IPgrp.Name)" = @()}
        $List = $IPgrp.Group.Ips | sort | select -Unique
        foreach ($Address in $List) {
            try {
                [string]$Parse = $Address
                [string]$IPAddress = $Parse.Substring(0,$Parse.IndexOf("/"))
                [string]$Cidr = $Parse.Substring($Parse.IndexOf("/")+1,$Parse.Length - $Parse.IndexOf("/")-1)
                [int]$Prefix = $Cidr
                if ($Prefix -eq 32) { [string]$Value = "network-object host $IPAddress" }
                else {
                    $mask = ([Math]::Pow(2,$Prefix)-1) * [Math]::Pow(2,(32-$Prefix))
                    $bytes = [BitConverter]::GetBytes([UInt32] $mask)
                    $IPMask = (($bytes.Count-1)..0 | ForEach-Object {[String]$bytes[$_]}) -join "."
                    [string]$Value = "network-object $IPAddress $IPMask"
                } # End if & else
                $Result."PortsUdp_$($IPgrp.Name)" += $Value.Trim()
            } # End Try
            catch { $ErrorList += "$Address --> $_" } # End Try & catch
        } # End foreach Address
    }
 
    if ($ErrorList) { 
        $ErrorList | Out-File "$Path\$date - Errors.txt"
        Add-Content $Report "$dateReport Error!!!"
    }   
    else { 
        $Result.Keys | % { $Result.Item($PSItem) | Out-File "$Path\$date - $_.txt" }
        Add-Content $Report "$dateReport OK"
        $Result.Keys | % {
            $Missing = @()
            $LatestRules = Get-Content "$Path\$date - $_.txt"
            if (Test-Path "$Path\$_.txt") {
                $LastFile = Get-Content "$Path\$_.txt"
                foreach ($line in $LatestRules) { if ($LastFile -notcontains $line)    {$Missing += $line} }
                foreach ($line in $LastFile)    { if ($LatestRules -notcontains $line) {$Missing += "no $line"} }
                if ($Missing) {$Missing | Out-File "$Path\$date - $_ Changes.txt"}
            }
            Copy-Item "$Path\$date - $_.txt" "$Path\$_.txt"
        }
        $ChangesToday = Get-ChildItem -Path $Path -Filter "*$date*Changes.txt"
        if ($ChangesToday) {
            foreach ($chg in $ChangesToday) { Add-Content -Value $chg.Name -Path "$Path\Report - Changes.txt" }
        }
    }
}

You need to change path in script (first line) to your custom folder.
You can run this script via Task Scheduler once per day.

Script will create multiple files (date is in format YYYYMMDD):
Report – Script logging (when was script run & result)
Report – Changes – List of changes (which list has changed)

It will also create file for each port group – currently there are 9 port groups (script will search for all port groups (TCP & UDP) and will create new files if needed). “- Changes” file will generate only if there are any changes)
– PortsTcp_25 (& [DATE] – PortsTcp_25, [DATE] – PortsTcp_25 – Changes)
– PortsTcp_80,443 (& [DATE] – PortsTcp_80,443, [DATE] – PortsTcp_80,443 – Changes)
– PortsTcp_143,993 (& [DATE] – PortsTcp_143,993, [DATE] – PortsTcp_143,993 – Changes)
– PortsTcp_443 (& [DATE] – PortsTcp_443, [DATE] – PortsTcp_443 – Changes)
– PortsTcp_587 (& [DATE] – PortsTcp_587, [DATE] – PortsTcp_587 – Changes)
– PortsTcp_995 (& [DATE] – PortsTcp_995, [DATE] – PortsTcp_995 – Changes)
– PortsTcp_50000-59999 (& [DATE] – PortsTcp_50000-59999, [DATE] – PortsTcp_50000-59999 – Changes)
– PortsUdp_3478,3479,3480,3481 (& [DATE] – PortsUdp_3478,3479,3480,3481, [DATE] – PortsUdp_3478,3479,3480,3481 – Changes)
– PortsUdp_50000-59999 (& [DATE] – PortsUdp_50000-59999, [DATE] – PortsUdp_50000-59999 – Changes)

Example of files generated (changes only in TCP ports 143,993 & 587 & UDP ports 50000-59999):
Reports

8 Comments

  1. Great to see this. Super useful. One minor ask I have is to use this to get the client identifier which retains the same GUID across calls. Since the web services are anonymous this is how we identify the number of machines that connect to it, it also may be required if there is a support request related to the web service.

    Simply replace this line, with the following two lines:
    $GUID = ([guid]::NewGuid()).Guid

    $d = $Env:TEMP + “\endpoints_guid.txt”
    if (Test-Path $d) { $GUID = Get-Content $d } else { $GUID = [GUID]::NewGuid().Guid; $GUID | Out-File $d }

    Paul Andrew

    Reply
  2. Thank you. great job

    Reply
  3. Hello Halis,

    Thank you for sharing your script.

    A minor point, but the “$NoIpv6” variable is never assigned a value. Based on some quick web browser tests, “NoIPv6=$NoIpv6” appears to be able to be shortened to just “NoIPv6”, i.e. the revised “$Link” assigned would be:

    $Link = “https://endpoints.office.com/endpoints/Worldwide?NoIPv6&ClientRequestId=$GUID”

    As Paul Andrew appears to be from Microsoft, I will add that if anyone from Microsoft happens to read this comment:

    It would be great for Microsoft to add the concept of “traffic direction” between on-premises systems and cloud to their REST API. It would be simpler to automatically update ASA rules knowing which IP blocks and ports needed to be opened in each direction. Some of the returned ranges will be for inbound traffic to on-prem (e.g. a hybrid Exchange set-up), some for outbound traffic, and some for traffic in both directions.

    Also beneficial would be a more clear definition of which ranges are for components such as “Exchange Online Protection”, which is only a subset of the “Exchange Online” service offerings. Relying on parsing for “urls” values of “*.mail.protection.outlook.com” and “*.protection.outlook.com” isn’t as clean as parsing on a dedicated “serviceAreaDisplayName” value such as “Exchange Online Protection” would be.

    Reply
  4. Thanks for this script, very helpful. There are a couple of entries in the Skype for Business/Teams section that use UDP ports 3478, 3479, 3480, 3481 so might need to generate one more file for these?

    Reply

Your email address will not be published.