Sophos Firewall Host Object Import Generator (Powershell)

This article was created with the help of a translation tool. Although we check the translation, errors in the content cannot be ruled out. Please bear this in mind when using the content. Thank you for your understanding.

With the help of this script for the "Sophos Firewall Host Object Import Generator", any number of host objects can be quickly added to an XML, which can then be quickly imported into the firewall - without an API. Tedious adding of many host objects is a thing of the past.

tested with SFOS 19.5.x to 21.0.x

Introduction

Many people will be familiar with this: Depending on how many hosts you have to create on the Sophos firewall, the effort can be very high. Sophos has two options for this purpose: the API and an XML configuration import.

I also use the API in some cases. However, the disadvantage here is that many people are not familiar with APIs, don't dare to use them, or the API is not configured for access. Personally, however, I am a friend of partial imports - they always work and require no preparation.

For this reason, I have created the PowerShell script below. It reads the host objects to be created from a CSV and prepares the data for import via the GUI. Maybe I will also offer an online version here in the future - let's see 😊

If you have any questions or problems with the script, please write in the comments.

Restrictions

The restrictions of the firewall apply to the input data.

  • Names can have a maximum of 60 characters
  • Descriptions can have a maximum of 255 characters

In addition, Script only accepts the following characters for the name:

  • a-zA-Z0-9_+-#!=*äüöÄÜÖß and the space character

The restriction of permitted characters can be removed when calling the script with the parameter -skipCharCheck. You must test yourself whether the additional characters then lead to errors.

This script only creates host objects and groups! For the creation of FQDN imports we have another article with another tool: Sophos Firewall FQDN Object Import Generator

Object Description

The description of objects has only been introduced with SFOS 20.0.1. Regardless of which version you use, the fields must appear in the CSV. Versions < SFOS 20.0.1 ignore the data during import. If the fields are not included in the CSV, the XMl import will not be created. Empty descriptions can be set by skipping the field with a double semicolon ;; and leaving it empty. Examples can be found in the demo data.
If objects are assigned to a group more than once, the first group must have a description. Subsequent descriptions for the same group are ignored.

CSV demo data

CSV demo data
Host Name;Host Description;Group Name;Group Description;Host Type;IP Address;IP Subnet;IP From;IP To;IP List
Host1;Description for Host1;Group_IP;Description for Group_IP;IP;192.168.1.1;;;;
Host5;;Group_IP;;IP;192.168.5.1;;;;
Host9;Description for Host9;Group_IP;;IP;192.168.9.1;;;;
Host2;Description for Host2;Group_Network;Description for Group_Network;Network;192.168.2.0;24;;;
Host6;;Group_Network;;Network;192.168.6.0;255.255.255.0;;;
Host10;Description for Host10;Group_Network;;Network;192.168.10.0;24;;;
Host3;Description for Host3;Group_Range;Description for Group_Range;Range;;;192.168.3.1;192.168.3.100;
Host7;Description for Host7;Group_Range;;Range;;;192.168.7.1;192.168.7.50;a
Host4;Description for Host4;Group_List;Description for Group_List;List;;;;;192.168.4.1,192.168.4.2,192.168.4.3
Host8;Description for Host8;Group_List;;List;;;;;192.168.8.1,192.168.8.2

Structure of the CSV

The header of the CSV can be included in the CSV or omitted. However, if the header is to be included, it must always contain this naming and must also remain in English:

Host Name;Host Description;Group Name;Group Description;Host Type;IP Address;IP Subnet;IP From;IP To;IP List

The CSV must always consist of 10 columns and must also be arranged in the specified order.

  1. Host name
    Display name of the host object (mandatory field)
  2. Host description
    The description of the host object (optional, is ignored for SFOS < 20.0.1 during import)
  3. Group name
    The display name of the host group to which the host is to be added (optional)
  4. Group description
    The description of the host group. If the group occurs more than once, only the description of the first occurrence is used! (Optional, only required once per host group)
  5. Host type
    The type of host object is selected here. Upper and lower case is not important. The names are self-explanatory. (compulsory)
    • IP ADDRESS
      Column 6 (IP address) is used here, 7-10 are ignored
    • Network
      Column 6 (IP address) must contain the network address and column 7 (subnet mask) the corresponding subnet mask in full notation (e.g.: 255.255.255.0) or in CIDR notation (e.g.: 24). Columns 8-10 are ignored
    • Range
      The Start IP must be in column 8 (IP from) and the End IP must be in column 9 (IP to). Columns 6-7, 10 are ignored
    • List
      The list must be separated by a comma in column 10. Columns 6-9 are ignored
  6. IP address
    The IP address (type IP) or netmask (type Network) - (mandatory for type IP or Network)
  7. Subnet mask
    The netmask - (mandatory for the Network type)
  8. IP from
    The start IP for an IP range (range type) - (mandatory for range type)
  9. IP to
    The end IP for an IP range (type Range) - (mandatory for type Range)
  10. IP LIste
    A comma-separated list of IP addresses - (mandatory for type List)

Empty columns are simply displayed with another semicolon. Please take a look at the CSV demo, as this illustrates the structure and possibilities well.

Powershell Script

As always, use of the script and the generated data is entirely at your own risk. We do not accept any guarantee or liability for errors that may occur. A test in a test environment is recommended in all cases.

Source code and download

PowerShell script to create an XML import from a CSV
<#
.DESCRIPTION
    Creates Sophos XML Import files for hosts objekts from a given CSV file

.PARAMETER -skipCharCheck
    Allow all characters in name and description

.PARAMETER -debug
    Print created hosts to console

.EXAMPLE
    Just start the script. Input and output file are prompted

.NOTES
    Demo CSV Data for input can be downloaded at: https://it-tech.wiki/shog

.LINK
    More information at https://it-tech.wiki/shog

.AUTHOR
    Sebastian Mies
    [email protected]

.LICENSE
    MIT License

    Copyright (c) 2024 Sebastian Mies

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
#>

param (
    [switch]$skipCharCheck,
    [switch]$debug
)


Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# Function to validate IPv4 Address
function Validate-IPv4Address {
    param (
        [string]$IPAddress
    )
    return [System.Net.IPAddress]::TryParse($IPAddress, [ref]$null)
}

# Function to validate Subnet Mask
function Validate-SubnetMask {
    param (
        [string]$subnetMask
    )
    $validMasks = @(
        "128.0.0.0", "192.0.0.0", "224.0.0.0", "240.0.0.0", "248.0.0.0",
        "252.0.0.0", "254.0.0.0", "255.0.0.0", "255.128.0.0", "255.192.0.0",
        "255.224.0.0", "255.240.0.0", "255.248.0.0", "255.252.0.0", "255.254.0.0",
        "255.255.0.0", "255.255.128.0", "255.255.192.0", "255.255.224.0", "255.255.240.0",
        "255.255.248.0", "255.255.252.0", "255.255.254.0", "255.255.255.0", "255.255.255.128",
        "255.255.255.192", "255.255.255.224", "255.255.255.240", "255.255.255.248", "255.255.255.252",
        "255.255.255.254", "255.255.255.255"
    )
    return $validMasks -contains $subnetMask
}

# Function to convert CIDR to full subnet mask
function Convert-CIDRToSubnetMask {
    param (
        [int]$CIDR
    )
    if ($CIDR -lt 0 -or $CIDR -gt 32) {
        throw "CIDR value must be between 0 and 32."
    }
    $binaryString = "1" * $CIDR + "0" * (32 - $CIDR)
    return ([convert]::ToInt32($binaryString.Substring(0, 8), 2)).ToString() + "." +
           ([convert]::ToInt32($binaryString.Substring(8, 8), 2)).ToString() + "." +
           ([convert]::ToInt32($binaryString.Substring(16, 8), 2)).ToString() + "." +
           ([convert]::ToInt32($binaryString.Substring(24, 8), 2)).ToString()
}

# Function to show open file dialog
function Show-OpenFileDialog {
    $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
    $OpenFileDialog.Filter = "CSV files (*.csv)|*.csv"
    $OpenFileDialog.ShowDialog() | Out-Null
    return $OpenFileDialog.FileName
}

# Function to show save file dialog
function Show-SaveFileDialog {
    $SaveFileDialog = New-Object System.Windows.Forms.SaveFileDialog
    $SaveFileDialog.Filter = "XML files (*.xml)|*.xml"
    $SaveFileDialog.ShowDialog() | Out-Null
    return $SaveFileDialog.FileName
}

# Function to validate allowed characters in strings
function Validate-AllowedCharacters {
    param (
        [string]$inputString
    )
    return $inputString -match '^[a-zA-Z0-9_+\-#!=* äüöÄÜÖß]+$'
}

# Ensure console output encoding is correct
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8


# Detect system language
$language = [System.Globalization.CultureInfo]::CurrentCulture.TwoLetterISOLanguageName
$isGerman = $language -eq 'de'

# Load messages
if ($isGerman) {
    $msgMissingHostName = "Zeile {0}: Host Name fehlt. Überspringen."
    $msgInvalidColumns = "Zeile {0}: Ungültige Spaltenanzahl. Erwartet: {1}, Tatsächlich: {2}. Überspringen."
    $msgInvalidHostName = "Zeile {0}, Host '{1}': Ungültige Zeichen im Host Name. Überspringen. Verwenden Sie -skipCharCheck, um diese Überprüfung zu umgehen."
    $msgInvalidHostDescription = "Zeile {0}, Host '{1}': Ungültige Zeichen in der Host-Beschreibung. Überspringen. Verwenden Sie -skipCharCheck, um diese Überprüfung zu umgehen."
    $msgInvalidGroupName = "Zeile {0}, Host '{1}': Ungültige Zeichen im Gruppenname. Überspringen. Verwenden Sie -skipCharCheck, um diese Überprüfung zu umgehen."
    $msgInvalidGroupDescription = "Zeile {0}, Host '{1}': Ungültige Zeichen in der Gruppenbeschreibung. Überspringen. Verwenden Sie -skipCharCheck, um diese Überprüfung zu umgehen."
    $msgTruncateHostName = "Zeile {0}, Host '{1}': Host Name überschreitet 60 Zeichen. Er wird gekürzt."
    $msgTruncateHostDescription = "Zeile {0}, Host '{1}': Host-Beschreibung überschreitet 255 Zeichen. Sie wird gekürzt."
    $msgMissingIpAddress = "Zeile {0}, Host '{1}': IP-Adresse fehlt für den Host-Typ 'IP'. Überspringen."
    $msgInvalidIpAddress = "Zeile {0}, Host '{1}': Ungültige IP-Adresse '{2}'. Überspringen."
    $msgMissingNetworkInfo = "Zeile {0}, Host '{1}': IP-Adresse oder Subnetz fehlt für den Host-Typ 'Network'. Überspringen."
    $msgInvalidNetworkAddress = "Zeile {0}, Host '{1}': Ungültige Netzwerkadresse '{2}'. Überspringen."
    $msgInvalidSubnetMask = "Zeile {0}, Host '{1}': Ungültige Subnetzmaske '{2}'. Überspringen."
    $msgInvalidCidr = "Zeile {0}, Host '{1}': Ungültiger CIDR-Wert '{2}'. Überspringen."
    $msgMissingRangeInfo = "Zeile {0}, Host '{1}': IP-From oder IP-To fehlt für den Host-Typ 'Range'. Überspringen."
    $msgInvalidIpRange = "Zeile {0}, Host '{1}': Ungültiger IP-Bereich '{2} - {3}'. Überspringen."
    $msgInvalidRangeOrder = "Zeile {0}, Host '{1}': Ungültiger IP-Bereich '{2} - {3}'. 'IP To' sollte größer als 'IP From' sein. Überspringen."
    $msgMissingIpList = "Zeile {0}, Host '{1}': IP-Liste fehlt für den Host-Typ 'List'. Überspringen."
    $msgIpListTooLong = "Zeile {0}, Host '{1}': IP-Liste überschreitet 1000 Einträge. Überspringen."
    $msgInvalidIpInList = "Zeile {0}, Host '{1}': Ungültige IP-Adresse '{2}' in der IP-Liste. Überspringen."
    $msgUnsupportedHostType = "Zeile {0}, Host '{1}': Nicht unterstützter Host-Typ '{2}'. Überspringen."
    $msgFileNotSelected = "XML-Dateipfad nicht ausgewählt."
    $msgXmlSaved = "XML-Datei wurde in {0} gespeichert"
    $msgTarSaved = "XML-Datei wurde in {0} gespeichert"
    $msgErrorTar = "Fehler beim Erstellen der TAR-Datei: {0}"
    $msgNoArchivingTool = "Kein geeignetes Archivierungstool gefunden. Bitte die Datei manuell in ein TAR-Archiv ohne Komprimierung mit dem Namen Entities.xml verschieben, bevor sie in die Firewall importiert wird."
    $msgCreatedXml = "XML-Datei erstellt: {0} - {1}"
} else {
    $msgMissingHostName = "Row {0}: Missing Host Name. Skipping."
    $msgInvalidColumns = "Row {0}: Invalid number of columns. Expected: {1}, Actual: {2}. Skipping."
    $msgInvalidHostName = "Row {0}, Host '{1}': Invalid characters in Host Name. Skipping. Use -skipCharCheck to bypass this check."
    $msgInvalidHostDescription = "Row {0}, Host '{1}': Invalid characters in Host Description. Skipping. Use -skipCharCheck to bypass this check."
    $msgInvalidGroupName = "Row {0}, Host '{1}': Invalid characters in Group Name. Skipping. Use -skipCharCheck to bypass this check."
    $msgInvalidGroupDescription = "Row {0}, Host '{1}': Invalid characters in Group Description. Skipping. Use -skipCharCheck to bypass this check."
    $msgTruncateHostName = "Row {0}, Host '{1}': Host Name exceeds 60 characters. It will be truncated."
    $msgTruncateHostDescription = "Row {0}, Host '{1}': Host Description exceeds 255 characters. It will be truncated."
    $msgMissingIpAddress = "Row {0}, Host '{1}': Missing IP Address for Host Type 'IP'. Skipping."
    $msgInvalidIpAddress = "Row {0}, Host '{1}': Invalid IP Address '{2}'. Skipping."
    $msgMissingNetworkInfo = "Row {0}, Host '{1}': Missing IP Address or Subnet for Host Type 'Network'. Skipping."
    $msgInvalidNetworkAddress = "Row {0}, Host '{1}': Invalid Network Address '{2}'. Skipping."
    $msgInvalidSubnetMask = "Row {0}, Host '{1}': Invalid Subnet Mask '{2}'. Skipping."
    $msgInvalidCidr = "Row {0}, Host '{1}': Invalid CIDR value '{2}'. Skipping."
    $msgMissingRangeInfo = "Row {0}, Host '{1}': Missing IP From or IP To for Host Type 'Range'. Skipping."
    $msgInvalidIpRange = "Row {0}, Host '{1}': Invalid IP Range '{2} - {3}'. Skipping."
    $msgInvalidRangeOrder = "Row {0}, Host '{1}': Invalid IP Range '{2} - {3}'. 'IP To' should be greater than 'IP From'. Skipping."
    $msgMissingIpList = "Row {0}, Host '{1}': Missing IP List for Host Type 'List'. Skipping."
    $msgIpListTooLong = "Row {0}, Host '{1}': IP List exceeds 1000 entries. Skipping."
    $msgInvalidIpInList = "Row {0}, Host '{1}': Invalid IP Address '{2}' in IP List. Skipping."
    $msgUnsupportedHostType = "Row {0}, Host '{1}': Unsupported Host Type '{2}'. Skipping."
    $msgFileNotSelected = "XML file path not selected."
    $msgXmlSaved = "XML file saved to {0}"
    $msgTarSaved = "XML file saved to {0}"
    $msgErrorTar = "Error creating TAR file: {0}"
    $msgNoArchivingTool = "No suitable archiving tool found. Please manually move the file into a TAR archive named Entities.xml without compression before importing it into the firewall."
    $msgCreatedXml = "Created XML: {0} - {1}"
}

# Load CSV file
$csvPath = Show-OpenFileDialog
if (-not $csvPath) {
    Write-Error $msgFileNotSelected
    exit
}

# Import CSV with semicolon delimiter
$csvData = Import-Csv -Path $csvPath -Delimiter ";"

# Check for correct number of columns
$requiredColumns = 10
$validData = @()
$firstLine = $true

foreach ($line in Get-Content -Path $csvPath) {
    # Ignore the first line if it contains the column headers
    if ($firstLine) {
        $firstLine = $false
        if ($line -match '^Host Name;Host Description;Group Name;Group Description;Host Type;IP Address;IP Subnet;IP From;IP To;IP List$') {
            continue
        }
    }

    $columns = $line -split ";"
    if ($columns.Length -ne $requiredColumns) {
        Write-Warning ($msgInvalidColumns -f $rowIndex, $requiredColumns, $columns.Length)
    } else {
        $validData += ,($line -split ";")
    }
}

# Initialize XML Document
[xml]$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDeclaration = $xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", $null)
$null = $xmlDoc.AppendChild($xmlDeclaration)

$configurationElement = $xmlDoc.CreateElement("Configuration")
$configurationElement.SetAttribute("APIVersion", "2000.2")
$configurationElement.SetAttribute("IS_WIFI6", "0")
$configurationElement.SetAttribute("IPS_CAT_VER", "1")
$null = $xmlDoc.AppendChild($configurationElement)

$hostGroups = @{}

# Process each row in CSV
$rowIndex = 0
foreach ($row in $validData) {
    $rowIndex++
    $hostName = $row[0].Trim()
    if (-not $hostName) {
        Write-Warning ($msgMissingHostName -f $rowIndex)
        continue
    }

    $hostDescription = $row[1].Trim()
    $groupName = $row[2].Trim()
    $groupDescription = $row[3].Trim()
    $hostType = $row[4].Trim()
    $ipAddress = $row[5].Trim()
    $ipSubnet = $row[6].Trim()
    $ipFrom = $row[7].Trim()
    $ipTo = $row[8].Trim()
    $ipList = $row[9].Trim()

    # Validate allowed characters in Host Name and Description
    if (-not $skipCharCheck) {
        if (-not (Validate-AllowedCharacters -inputString $hostName)) {
            Write-Warning ($msgInvalidHostName -f $rowIndex, $hostName)
            continue
        }
        if ($hostDescription -and -not (Validate-AllowedCharacters -inputString $hostDescription)) {
            Write-Warning ($msgInvalidHostDescription -f $rowIndex, $hostName)
            continue
        }
        if ($groupName -and -not (Validate-AllowedCharacters -inputString $groupName)) {
            Write-Warning ($msgInvalidGroupName -f $rowIndex, $hostName)
            continue
        }
        if ($groupDescription -and -not (Validate-AllowedCharacters -inputString $groupDescription)) {
            Write-Warning ($msgInvalidGroupDescription -f $rowIndex, $hostName)
            continue
        }
    }

    # Validate and truncate Host Name
    if ($hostName.Length -gt 60) {
        Write-Warning ($msgTruncateHostName -f $rowIndex, $hostName)
        $hostName = $hostName.Substring($hostName.Length - 60)
    }

    # Validate and truncate Host Description
    if ($hostDescription.Length -gt 255) {
        Write-Warning ($msgTruncateHostDescription -f $rowIndex, $hostName)
        $hostDescription = $hostDescription.Substring($hostDescription.Length - 255)
    }

    # Validate Host Type and required fields
    $isValid = $true
    switch -Regex ($hostType.ToLower()) {
        '^ip$' {
            if (-not $ipAddress) {
                Write-Warning ($msgMissingIpAddress -f $rowIndex, $hostName)
                $isValid = $false
            }
            elseif (-not (Validate-IPv4Address -IPAddress $ipAddress)) {
                Write-Warning ($msgInvalidIpAddress -f $rowIndex, $hostName, $ipAddress)
                $isValid = $false
            }
            else {
                $hostType = "IP"
            }
        }
        '^network$' {
            if (-not $ipAddress -or -not $ipSubnet) {
                Write-Warning ($msgMissingNetworkInfo -f $rowIndex, $hostName)
                $isValid = $false
            }
            elseif (-not (Validate-IPv4Address -IPAddress $ipAddress)) {
                Write-Warning ($msgInvalidNetworkAddress -f $rowIndex, $hostName, $ipAddress)
                $isValid = $false
            }
            else {
                try {
                    if ($ipSubnet -match '^\d+$') {
                        $ipSubnetInt = [int]$ipSubnet
                        $ipSubnet = Convert-CIDRToSubnetMask -CIDR $ipSubnetInt
                    } 
                    elseif (-not (Validate-SubnetMask -subnetMask $ipSubnet)) {
                        Write-Warning ($msgInvalidSubnetMask -f $rowIndex, $hostName, $ipSubnet)
                        $isValid = $false
                    }
                } catch {
                    Write-Warning ($msgInvalidCidr -f $rowIndex, $hostName, $ipSubnet)
                    $isValid = $false
                }
                $hostType = "Network"
            }
        }
        '^range$' {
            if (-not $ipFrom -or -not $ipTo) {
                Write-Warning ($msgMissingRangeInfo -f $rowIndex, $hostName)
                $isValid = $false
            }
            elseif (-not (Validate-IPv4Address -IPAddress $ipFrom) -or -not (Validate-IPv4Address -IPAddress $ipTo)) {
                Write-Warning ($msgInvalidIpRange -f $rowIndex, $hostName, $ipFrom, $ipTo)
                $isValid = $false
            }
            else {
                $startIP = [System.Net.IPAddress]::Parse($ipFrom).GetAddressBytes()
                $endIP = [System.Net.IPAddress]::Parse($ipTo).GetAddressBytes()
                if ([BitConverter]::ToUInt32($startIP, 0) -ge [BitConverter]::ToUInt32($endIP, 0)) {
                    Write-Warning ($msgInvalidRangeOrder -f $rowIndex, $hostName, $ipFrom, $ipTo)
                    $isValid = $false
                }
                $hostType = "IPRange"
            }
        }
        '^list$' {
            if (-not $ipList) {
                Write-Warning ($msgMissingIpList -f $rowIndex, $hostName)
                $isValid = $false
            }
            else {
                $ipListArray = $ipList -split ',\s*'
                if ($ipListArray.Length -gt 1000) {
                    Write-Warning ($msgIpListTooLong -f $rowIndex, $hostName)
                    $isValid = $false
                }
                else {
                    $validIPs = $ipListArray | ForEach-Object {
                        if (-not (Validate-IPv4Address -IPAddress $_)) {
                            Write-Warning ($msgInvalidIpInList -f $rowIndex, $hostName, $_)
                            $false
                        } else {
                            $true
                        }
                    }
                    if ($validIPs -contains $false) {
                        $isValid = $false
                    }
                }
                $hostType = "IPList"
            }
        }
        default {
            Write-Warning ($msgUnsupportedHostType -f $rowIndex, $hostName, $hostType)
            $isValid = $false
        }
    }

    # If valid, create IPHost element
    if ($isValid) {
        $ipHostElement = $xmlDoc.CreateElement("IPHost")
        $ipHostElement.SetAttribute("transactionid", "")

        $nameElement = $xmlDoc.CreateElement("Name")
        $nameElement.InnerText = $hostName
        $null = $ipHostElement.AppendChild($nameElement)

        $descriptionElement = $xmlDoc.CreateElement("Description")
        if ($hostDescription) {
            $descriptionElement.InnerText = $hostDescription
        } else {
            $descriptionElement.IsEmpty = $true
        }
        $null = $ipHostElement.AppendChild($descriptionElement)

        $ipFamilyElement = $xmlDoc.CreateElement("IPFamily")
        $ipFamilyElement.InnerText = "IPv4"
        $null = $ipHostElement.AppendChild($ipFamilyElement)

        $hostTypeElement = $xmlDoc.CreateElement("HostType")
        $hostTypeElement.InnerText = $hostType
        $null = $ipHostElement.AppendChild($hostTypeElement)

        switch ($hostType) {
            "IP" {
                $ipAddressElement = $xmlDoc.CreateElement("IPAddress")
                $ipAddressElement.InnerText = $ipAddress
                $null = $ipHostElement.AppendChild($ipAddressElement)
            }
            "Network" {
                $ipAddressElement = $xmlDoc.CreateElement("IPAddress")
                $ipAddressElement.InnerText = $ipAddress
                $null = $ipHostElement.AppendChild($ipAddressElement)

                $subnetElement = $xmlDoc.CreateElement("Subnet")
                $subnetElement.InnerText = $ipSubnet
                $null = $ipHostElement.AppendChild($subnetElement)
            }
            "IPRange" {
                $startIPAddressElement = $xmlDoc.CreateElement("StartIPAddress")
                $startIPAddressElement.InnerText = $ipFrom
                $null = $ipHostElement.AppendChild($startIPAddressElement)

                $endIPAddressElement = $xmlDoc.CreateElement("EndIPAddress")
                $endIPAddressElement.InnerText = $ipTo
                $null = $ipHostElement.AppendChild($endIPAddressElement)
            }
            "IPList" {
                $listOfIPAddressesElement = $xmlDoc.CreateElement("ListOfIPAddresses")
                $listOfIPAddressesElement.InnerText = ($ipListArray -join ",")
                $null = $ipHostElement.AppendChild($listOfIPAddressesElement)
            }
        }

        $null = $configurationElement.AppendChild($ipHostElement)

        if ($debug) {
            Write-Host ($msgCreatedXml -f $hostName, $hostType) -ForegroundColor Blue
        }
    }

    # Process Group Name and Description
    if ($groupName) {
        if ($groupName.Length -gt 60) {
            Write-Warning ($msgTruncateHostName -f $rowIndex, $groupName)
            $groupName = $groupName.Substring($groupName.Length - 60)
        }

        if ($groupDescription.Length -gt 255) {
            Write-Warning ($msgTruncateHostDescription -f $rowIndex, $groupName)
            $groupDescription = $groupDescription.Substring($groupDescription.Length - 255)
        }

        if (-not $hostGroups.ContainsKey($groupName)) {
            $hostGroups[$groupName] = @{
                Description = $groupDescription
                Hosts = @()
            }
        }

        $hostGroups[$groupName].Hosts += $hostName
    }
}

# Create IPHostGroup elements
foreach ($groupName in $hostGroups.Keys) {
    $group = $hostGroups[$groupName]

    $ipHostGroupElement = $xmlDoc.CreateElement("IPHostGroup")
    $ipHostGroupElement.SetAttribute("transactionid", "")

    $groupNameElement = $xmlDoc.CreateElement("Name")
    $groupNameElement.InnerText = $groupName
    $null = $ipHostGroupElement.AppendChild($groupNameElement)

    $groupDescriptionElement = $xmlDoc.CreateElement("Description")
    if ($group.Description) {
        $groupDescriptionElement.InnerText = $group.Description
    } else {
        $groupDescriptionElement.IsEmpty = $true
    }
    $null = $ipHostGroupElement.AppendChild($groupDescriptionElement)

    $hostListElement = $xmlDoc.CreateElement("HostList")
    foreach ($hostName in $group.Hosts) {
        $hostElement = $xmlDoc.CreateElement("Host")
        $hostElement.InnerText = $hostName
        $null = $hostListElement.AppendChild($hostElement)
    }
    $null = $ipHostGroupElement.AppendChild($hostListElement)

    $ipFamilyElement = $xmlDoc.CreateElement("IPFamily")
    $ipFamilyElement.InnerText = "IPv4"
    $null = $ipHostGroupElement.AppendChild($ipFamilyElement)

    $null = $configurationElement.AppendChild($ipHostGroupElement)

    if ($debug) {
        Write-Host ($msgCreatedXml -f $groupName, "Group") -ForegroundColor Blue
    }
}

# Save XML to file
$xmlPath = Show-SaveFileDialog
if (-not $xmlPath) {
    Write-Error $msgFileNotSelected
    exit
}
$entitiesXmlPath = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($xmlPath), "Entities.xml")
$xmlDoc.Save($entitiesXmlPath)
Write-Host ($msgXmlSaved -f $entitiesXmlPath) -ForegroundColor Green


# Create TAR archive
$tarPath = [System.IO.Path]::ChangeExtension($xmlPath, ".tar")

# Check for tar.exe
$tarExePath = "$env:SystemRoot\System32\tar.exe"
$tarCreated = $false
if (Test-Path $tarExePath) {
    $startInfo = New-Object System.Diagnostics.ProcessStartInfo
    $startInfo.FileName = $tarExePath
    $startInfo.Arguments = "-cf `"$tarPath`" -C `"$([System.IO.Path]::GetDirectoryName($entitiesXmlPath))`" Entities.xml"
    $startInfo.RedirectStandardOutput = $true
    $startInfo.RedirectStandardError = $true
    $startInfo.UseShellExecute = $false
    $startInfo.CreateNoWindow = $true

    $process = [System.Diagnostics.Process]::Start($startInfo)
    $stdout = $process.StandardOutput.ReadToEnd()
    $stderr = $process.StandardError.ReadToEnd()
    $process.WaitForExit()

    if ($process.ExitCode -eq 0 -and (Test-Path $tarPath)) {
        $tarCreated = $true
        Write-Host ($msgTarSaved -f $tarPath) -ForegroundColor Green
    } else {
        Write-Error ($msgErrorTar -f $stderr)
    }
}

# Check for 7-Zip in standard and other paths
if (-not $tarCreated) {
    $sevenZipPaths = @(
        "$env:ProgramFiles\7-Zip\7z.exe",
        "$env:ProgramFiles(x86)\7-Zip\7z.exe"
    )
    $sevenZipPath = $sevenZipPaths | Where-Object { Test-Path $_ }
    if (-not $sevenZipPath) {
        $sevenZipPath = Get-Command 7z -ErrorAction SilentlyContinue
    }
    if ($sevenZipPath) {
        $startInfo = New-Object System.Diagnostics.ProcessStartInfo
        $startInfo.FileName = $sevenZipPath
        $startInfo.Arguments = "a -ttar `"$tarPath`" `"$entitiesXmlPath`""
        $startInfo.RedirectStandardOutput = $true
        $startInfo.RedirectStandardError = $true
        $startInfo.UseShellExecute = $false
        $startInfo.CreateNoWindow = $true

        $process = [System.Diagnostics.Process]::Start($startInfo)
        $stdout = $process.StandardOutput.ReadToEnd()
        $stderr = $process.StandardError.ReadToEnd()
        $process.WaitForExit()

        if ($process.ExitCode -eq 0 -and (Test-Path $tarPath)) {
            $tarCreated = $true
            Write-Host ($msgTarSaved -f $tarPath) -ForegroundColor Green
        } else {
            Write-Error ($msgErrorTar -f $stderr)
        }
    }
}

# If no suitable archive tool is found
if (-not $tarCreated) {
    Write-Warning $msgNoArchivingTool
}

# Delete the additional Entities.xml file if it is not the original XML
if ($entitiesXmlPath -ne $xmlPath) {
    Remove-Item -Path $entitiesXmlPath -Force
}
Expand
Sophos Host Objekt Generator (Version: 1.1)
24.67 KB

Use of the script

The script is simply called up in Powershell. The input file (CSV) and the output file (XML) are queried interactively via a dialog.
If the tar.exe exists in the System32 directory on the system, or 7zip is installed, the TAR file required for the import into the firewall is also created.

No TAR file received?

If neither tar.exe nor 7zip is available on the system, you must pack the output file yourself as Entities.xml in an uncompressed TAR archive. If the file in the archive has a different name, the import will be ignored!

Error: ExecutionPolicy / not digitally signed

If you receive an error that indicates a regulation by the ExecutionPolicy, you must adjust the ExecutionPolicy with Set-ExecutionPolicy. Our script is not signed. The error is therefore to be expected on a standard system. The adjustment should always be carried out by experienced personnel and, if possible, reversed afterwards. However, we will deliberately not provide more detailed information on customization and recommendations at this point. So either use Google or speak to experienced personnel on the subject. We ask for your understanding!

Parameters for calling

  • -skipCharCheck
    Does not check the permitted characters.
  • -debug
    Outputs in the command line which hosts or groups have been read in

Importing the XML into the firewall

The file can be imported into the firewall via the web interface. The hosts are then created.

  • The TAR file must always be uploaded. Other formats are ignored. No TAR archive received? Then look under"No TAR file received"
  • If there are a lot of hosts in the imported file, the import may take a little longer. In general, however, the speed also depends on the hardware used! So be patient if necessary.
Importing the XML file

FAQ

What about FQDN objects?

FQDNs are not created by this script. However, we have another tool for this purpose. Just have a look here: Sophos Firewall FQDN Object Import Generator

Error with the ExecutionPolicy / no digital signature

You can find out more in the article above.

I have found a mistake

Great, you've found the Easteregg 🥳
No, joking aside... Errors cannot be ruled out due to the number of possible test scenarios. If you have found a bug, please let us know via the contact form or in the comments which data lead to the error and we will make sure that we fix the bug as soon as possible.

Is something missing?

If you are missing something in the generator, please write to us via the contact form or in the comments. Let's see if your feature request makes it in 😊

Where and how is the XML imported?

Please upload and import the created file (.tar) as it is under "Backup & Firmware -> Import/Export". After the message that the process was successful, the desired objects have been created.

FQDN Object Import Generator Import XML -

How can it be ensured that the import does not install a backdoor or anything else?

Simply unzip the TAR file and open the XML file in the editor. You will only find the data you have sent in here.

More interesting articles

3 Comments

Leave a Reply

Comments are not displayed directly, as they are released in moderation.


  1. Danke für das Script!!!
    Ich habe schon den FQDN Generator mit Freude benutzt. Jetzt eine Möglichkeit schnell die Hosts anzulegen. Einfach Top! Bin schon gespannt was hier noch so kommt. 😀

    • Danke für Dein Feedback, und prima, dass es auch außerhalb meiner eigenen Tests funtioniert! 😊
      Ein paar Ideen habe ich noch. Mal sehen wann ich weiter Zeit finde…

WordPress Cookie Plugin by Real Cookie Banner