Sophos Firewall: Importing User definitions into Sophos Firewall after v18.0 MR3 and v17.5 MR14

Disclaimer: This information is provided as-is for the benefit of the Community. Please contact Sophos Professional Services if you require assistance with your specific environment.


Overview

In recent MR releases there have been several changes to improve the security of Sophos Firewall and to make it more difficult for attackers to get hold of sensitive information if something does go wrong. 

One of the changes was to remove the ability to export and import user accounts using csv files. There were several security concerns about this feature, in particular the added risk of exposure of password material that it encourages. Many customers now manage users by connecting their firewalls to an external directory service, such as Active Directory, which overall creates a much more secure and manageable environment.

However some customers have established processes that rely on importing lists of users from time to time. What can they do?

SFOS has an XML API that provides a way to automatically manage most objects and features that can be controlled via the Webadmin user interface, and User objects are no exception.

Using the XML API is a great alternative to the old CSV import feature for bulk adding of on-device user accounts.

But how? Well, here's an example. The attached Python program can read basic user data from a csv file and post it directly to your firewall's XML API. It basically replicates the old CSV import functionality.

*NOTE* If Python is not your thing, please check out the reply below in this thread where I added a Powershell version of this script. This can be run directly in Powershell on Windows without installing any other software.

If you have a process where, from time to time, you create a csv file with new user information in it, you can now use this script instead to import it directly instead of navigating through the WebAdmin UI.

PLEASE NOTE: The attached script does not currently work on SFOS installations where the Default Configuration Language is not English. I am working on an update and will post it here soon.

Here's how to do it. Not all steps will be required for everyone - if you already use Python, or if you use a Mac that has Python built-in, you should be pretty much set to go. 

Enabling XML API on your firewall

  1. Log in to your Sophos Firewall as an Administrator account
  2. Navigate to Backup & firmware > API
  3. Under API configuration, check the 'Enabled' box
  4. Under Allowed IP address, enter the IP address of the computer where you are going to run this program

Installing Python 3 and preparing dependencies

  1. Install Python 3 on your computer.
    1. Python 3 comes built in to some Unix-based systems, including MacOS.
    2. For Windows systems, you can download and install the latest Python installer from https://www.python.org
    3. For Linux distributions, your system's default package manager will almost certainly have a suitable package ready to install.
  2. When installing Python on Windows, make sure to select the option to "Add Python to PATH" on the first screen of the installer. This will allow you to run python scripts from a Windows command prompt.
  3. After installation completes, open a new Command Prompt window.
  4. Install additional libraries that are required to run this program - run
    C:\Users\John> pip install requests python-certifi-win32
  5. If you have enabled TLS decryption on your Sophos Firewall and get a certificate error running this command, you can either create a decryption exclusion for the domain pypi.org or get a copy of your firewall's root CA certificate in a file and rerun the command as follows:
    C:\Users\John> pip install requests python-certifi-win32 --cert mycacert.pem

Prepare your CSV file

This script expects you to provide a csv file, with fields separated by commas. Users with regional preferences that create csv files with semicolons or other characters may find that it works, or may need to adjust the script accordingly.

The file must have the following columns - Name, Username, Password, Email Address, Group:

Example file:

Name,Username,Password,Email Address,Group
Nigel Brown,nbrown,Pa5s!w0rd19,nigel.brown@example.com,Open Group
Gina Lopez,glopez,e1Azjr8q9^21,gina.lopez@example.com,Open Group

Downloading and running the script

Download this zip file and extract it to a directory on your computer. The rest of these instructions assume you saved it in 'Downloads', and that the csv file containing the users you want to add is in 'Documents' and called users.csv

Run the following command, substituting your firewall's hostname or IP address, admin username and password. If you're using an account with multi-factor authentication (OTP) then don't forget to append the authentication code to the end of the password:

C:\Users\John> python Downloads\UserImport.py -f myfirewall.example.com -i Documents\users.csv -u admin -p A1B2c3d4!!E5 -a

If you see exception messages related to certificate trust issues, try running the same command again, but add '-n' as an additional command-line qualifier. The argument '-n' tells Python to ignore any certificate validation errors when connecting to the firewall - python does not automatically pick up any additional root CAs from your Windows installation:

C:\Users\John> python Downloads\UserImport.py -f myfirewall.example.com -i Documents\users.csv -u admin -p A1B2c3d4!!E5 -a -n

As long as the details of all the users are correct, the import should proceed successfully. If it does not, check the message responses for clues as to what went wrong.

Troubleshooting

  • Error code 534: API operations are not allowed from the requester IP address.
    • You need to add the IP address of your computer to the 'Allowed IP' list on your Sophos Firewall at Backup & firmware > API
  • Login status: Authentication Failure
    • The username or password were incorrect.
    • The user specified is not an Administrator.
    • If the user is configured to use multi-factor authentication using one-time passwords, you'll need to add the current authentication code at the end of the user's password at the command line.
  • Error code 532: You need to enable the API Configuration.
    • You need to enable API configuration as shown in the steps above, under "Enabling XML API on your firewall"
  • Status 599, Message: Not having privilege to add/modify configuration.
    • The admin account specified is configured with a Device access profile that does not allow 'Write' operations for User objects.
  • Status 502, Message: A user with the same name already exists
    • You are in add mode (with the command line argument '-a') and there is already a user matching the name specified in your csv
    • If you want to update existing user records with the same name, omit the '-a' command line argument. This tells the script to send an 'update' command instead of an 'add' command to the XML API.
  • Status 511, Message: Operation failed. Please contact Support.
    • Run the following command to check applog.log:

      grep "common-password db" /log/applog.log

      Example:

      XGS107_SN01_SFOS 19.0.1 MR-1-Build365# grep "common-password db" /log/applog.log
      Nov 30 18:11:38Z Given password is found in common-password db, Try with different password

    • If this log line matches with the recent timestamp, then try using a different password in the csv file.

Python code

If you don't want to download the code as a zip file, you can copy and paste it from here:

# UserImport
#
# Takes basic User definitions from a CSV file and posts
# them to an XG Firewall using the XML API

import xml.etree.ElementTree as ET
import argparse
import requests
import sys
import configparser
import csv

serial = 0

UserAPIAddResults = {
    "200": (True, "User registered successfully"),
    "500": (False, "User couldn't be registered"),
    "502": (False, "A user with the same name already exists"),
    "503": (False, "A user with the same L2TP/PPTP IP already exists"),
    "510": (False, "Invalid password - doesn't meet complexity requirements")
}

UserAPIUpdateResults = {
    "200": (True, "User registered successfully"),
    "500": (False, "User couldn't be updated"),
    "502": (False, "A user with the same name already exists"),
    "503": (False, "A user with the same L2TP/PPTP IP already exists"),
    "510": (False, "Invalid password - doesn't meet complexity requirements"),
    "541": (False, "There must be at least one Administrator"),
    "542": (False,
            "There must be at least one user with Administrator profile")
}


def eprint(*args, **kwargs):
    # Print to stderr instead of stdout
    print(*args, file=sys.stderr, **kwargs)


def getArguments():
    parser = argparse.ArgumentParser(
        description='Grab a list of video IDs from a YouTube playlist')

    parser.add_argument(
        '-f', '--firewall',
        help=("To call the API directly, specify a firewall hostname or IP. "
              "Without this, an XML API document will be output to stdout."),
        default=argparse.SUPPRESS)
    parser.add_argument(
        '-i', '--input', metavar="FILENAME",
        help=("Name of CSV file containing these columns:"
              " Name, Username, Password, Email Address, Group"),
        default=argparse.SUPPRESS)
    parser.add_argument(
        '-u', '--fwuser', metavar="ADMIN-USERNAME",
        help='Admin username for the XG firewall',
        default='admin')
    parser.add_argument(
        '-p', '--fwpassword', metavar="ADMIN-PASSWORD",
        help='Password for the XG user - defaults to "admin"',
        default='password')
    parser.add_argument(
        '-a', '--add',
        help=('Call API in "Add" mode - use first time only (otherwise'
              ' Update will be used)'),
        action='store_true')
    parser.add_argument(
        '-n', '--insecure',
        help="Don't validate the Firewall's HTTPS certificate",
        action='store_false')
    parser.add_argument(
        '-1', '--oneshot',
        help=("Create a single enormous XMLAPI transaction instead "
              " of multiple smaller ones. Only used when outputting "
              "to stdout "),
        action='store_true')
    parser.add_argument(
        '-x', '--useimpex',
        help=("Create an Entities.xml-style file for inclusion in an "
              " Import tarball"),
        action='store_true')

    return parser.parse_args()


def readConfig():
    # Read in the temp config file (if it exists)
    global config
    global workingPath

    config = configparser.ConfigParser()
    config.read(workingPath)


def getserial():
    # Returns an incrementing serial number. Used to reference individual
    # XMLAPI transactions.
    global serial
    serial = serial + 1
    return str(serial)


def xgAPIStartUser(username, name, password, email,
                   group="Open Group", usertype="User",
                   surfquota="Unlimited Internet Access",
                   accesstime="Allowed all the time",
                   datatransferpolicy="", qospolicy="", sslvpnpolicy="",
                   clientlesspolicy="", status="Active", l2tp="Disable",
                   pptp="Disable", cisco="Disable",
                   quarantinedigest="Disable", macbinding="Disable",
                   loginrestriction="UserGroupNode",
                   accessschedule="All The Time", loginrestrictionapp="",
                   isencryptcert="Disable", simultaneouslogins="Enable"):
    # Returns a complete XML User record with some sane defaults which can
    # be modified later, if you want.

    userblock = ET.Element('User', transactionid=getserial())
    ET.SubElement(userblock, 'Username').text = username
    ET.SubElement(userblock, 'Name').text = name
    ET.SubElement(userblock, 'Password').text = password
    ET.SubElement(ET.SubElement(userblock, 'EmailList'),
                  'EmailID').text = email
    ET.SubElement(userblock, 'Group').text = group
    ET.SubElement(userblock, 'SurfingQuotaPolicy').text = surfquota
    ET.SubElement(userblock, 'AccessTimePolicy').text = accesstime
    ET.SubElement(userblock, 'DataTransferPolicy').text = datatransferpolicy
    ET.SubElement(userblock, 'QoSPolicy').text = qospolicy
    ET.SubElement(userblock, 'SSLVPNPolicy').text = sslvpnpolicy
    ET.SubElement(userblock, 'ClientlessPolicy').text = clientlesspolicy
    ET.SubElement(userblock, 'Status').text = status
    ET.SubElement(userblock, 'L2TP').text = l2tp
    ET.SubElement(userblock, 'PPTP').text = pptp
    ET.SubElement(userblock, 'CISCO').text = cisco
    ET.SubElement(userblock, 'QuarantineDigest').text = quarantinedigest
    ET.SubElement(userblock, 'MACBinding').text = macbinding
    ET.SubElement(userblock, 'LoginRestriction').text = loginrestriction
    ET.SubElement(
        userblock, 'ScheduleForApplianceAccess').text = accessschedule
    ET.SubElement(
        userblock, 'LoginRestrictionForAppliance').text = loginrestrictionapp
    ET.SubElement(userblock, 'IsEncryptCert').text = isencryptcert
    ET.SubElement(
        userblock, 'SimultaneousLoginsGlobal').text = simultaneouslogins

    return userblock


def xgAPILogin(fwuser, fwpassword):
    # Returns a root Login element for an XMLAPI call

    requestroot = ET.Element('Request')
    login = ET.SubElement(requestroot, 'Login')
    ET.SubElement(login, 'Username').text = fwuser
#  ET.SubElement(login, 'Password', passwordform = 'encrypt').text = fwpassword
    ET.SubElement(login, 'Password').text = fwpassword

    return requestroot


def xgImpExBegin():
    return ET.Element('Configuration', APIVersion="1702.1", IPS_CAT_VER="1")


def xgAPIPost(requestroot):
    # Posts an XMLAPI document to the firewall specified in command line -f arg
    # If there is no -f arg, it prints the document to stdout
    # Parameter 'requestroot' provides the root element of the XML document

    postdata = {
        'reqxml': ET.tostring(requestroot, 'unicode')
    }
    result = 0

    try:
        callurl = ('https://' + stuff.firewall +
                   ':4444/webconsole/APIController')
        eprint("Sending XMLAPI request to %s" % callurl)

        r = requests.post(callurl, data=postdata, verify=stuff.insecure)
#        eprint(r)
#        eprint(r.text)
        result = 1
    except AttributeError:
        print(ET.tostring(requestroot, 'unicode'))
        result = 0
    return (result, r)

# Fun starts here


# Check command line
stuff = getArguments()

try:
    eprint("Reading from file: %s" % stuff.input)
except AttributeError:
    eprint("No filename provided. Run this command with --help for more info.")
    sys.exit()

# If there's a firewall address set, make sure we're in oneshot mode
try:
    eprint("Config will be posted to " + stuff.firewall)
    stuff.oneshot = True
except AttributeError:
    eprint("No firewall set")

if stuff.add:
    method = 'add'
else:
    method = 'update'

# Start building the XMLAPI Request
if stuff.useimpex:
    fwRequestroot = xgImpExBegin()
    fwRequestSet = fwRequestroot
else:
    fwRequestroot = xgAPILogin(stuff.fwuser, stuff.fwpassword)
    fwRequestSet = ET.SubElement(fwRequestroot, 'Set', operation=method)

usernames = []

# Open the csv for reading
with open(stuff.input, encoding="utf-8-sig") as input_csv:
    csv_parser = csv.DictReader(input_csv, delimiter=',')
    count = 0
    for row in csv_parser:
        #       eprint(row)

        fwRequestSet.append(xgAPIStartUser(row["Username"], row["Name"],
                                           row["Password"],
                                           row["Email Address"],
                                           group=row["Group"]))
        usernames.append(row["Username"])
        count = count + 1

    eprint("Read %d users from file %s\n" % (count, stuff.input))

if stuff.oneshot or stuff.useimpex:
    result, r = xgAPIPost(fwRequestroot)

if result == 1:
    resultcontent = ET.fromstring(r.text)
    for child in resultcontent:
        if child.tag == "Login":
            status = child.find('status').text
            if status == '200':
                eprint("API login successful")
            else:
                eprint("Login status: %s" % child.find('status').text)

        if child.tag == "User":
            Status = child.find('Status')
            eprint("Line %s (%s), Status %s" % (
                child.attrib["transactionid"], usernames[int(
                    child.attrib["transactionid"]) - 1],
                Status.attrib["code"]))
            try:
                if stuff.add:
                    eprint("     %s" %
                           UserAPIAddResults[Status.attrib["code"]][1])
                else:
                    eprint("     %s" %
                           UserAPIUpdateResults[Status.attrib["code"]][1])
            except KeyError:
                eprint("Message: %s" % Status.text)
                continue

        if child.tag == "Status":
            eprint("Error code %s: %s" % (child.attrib['code'], child.text))

# Take the xml output from this program and send it to your firewall with curl,
# for example:
# $ curl -k https://<firewall ip>:4444/webconsole/APIController -F "reqxml=<foo.xml"

# To create an API Import file, use '-x', write the XML output to a file
# 'Entities.xml' and create a tarball with the following (group/owner options 
# to make it anonymous):
# $ tar --group=a:1000 --owner=a:1000 --numeric-owner -cvf ../API-O365.tar Entities.xml

______________________________________________________________________________________________________________________________________



Added horizontal lines, edited format, edited table of contents, minor grammar edit
[edited by: Raphael Alganes at 1:53 PM (GMT -8) on 4 Dec 2023]
  • We totally agree with NickL. Sophos is started to make life harder for us with each new upgrade.
    It is such an important case for us that if they do not bring these feature back, we strongly bring it into agenda to replace more than hundreds of Sophos device under our account with another brand of device.

  • Hi,

    just followed this guide  XG430 (SFOS 18.0.3 MR-3)  

     with the exact same csv example and i got this error from de cmd line:  

    Login status: Authentication Successful Line 1 (nbrown), Status 501 Message: Configuration parameters validation failed. Line 2 (glopez), Status 501 Message: Configuration parameters validation failed.

     after checking apiparser.log about this 501 Status i found this: 

    INFO Feb 15 16:25:21 [14353]: Start Login Handler,Component : Login
    ERROR Feb 15 16:25:21 [14353]: Key:ISCrEntity is not found in RequestMap File for Login.
    INFO Feb 15 16:25:21 [14353]: Mapping file for Login component is /_conf/csc/IOMappingFiles//1800.2/Login/Login.xml
    ERROR Feb 15 16:25:21 [14353]: Flag setting for this opcode is 18.
    INFO Feb 15 16:25:22 [14353]: Opcode response: status:200
    INFO Feb 15 16:25:22 [14353]: Authentication Successful
    INFO Feb 15 16:25:22 [14353]: Start Set Handler,Component : User
    ERROR Feb 15 16:25:22 [14353]: Key:ISCrEntity is not found in RequestMap File for User.
    ERROR Feb 15 16:25:22 [14353]: Parser Error: xmlvalue for jsonkey="description", xmlelement="/User/Description" cannot be found in request file.
    ERROR Feb 15 16:25:22 [14353]: Parser Error: xmlvalue for jsonkey="securitylevel", xmlelement="/User/UserType" cannot be found in request file.
    ERROR Feb 15 16:25:22 [14353]: Parser Error: xmlvalue for jsonkey="maclist", xmlelement="/User/MACAddressList/MACAddress" cannot be found in request file.
    ERROR Feb 15 16:25:22 [14353]: json object not found with key="securitylevel" to handle logicaloperator.
    ERROR Feb 15 16:25:22 [14353]: json object not found with key="securitylevel" to handle logicaloperator.
    ERROR Feb 15 16:25:22 [14353]: json object not found with key="securitylevel" to handle logicaloperator.
    ERROR Feb 15 16:25:22 [14353]: type != const in logicaloperator.So string comparision is done.
    ERROR Feb 15 16:25:22 [14353]: type != const in logicaloperator.So string comparision is done.
    ERROR Feb 15 16:25:22 [14353]: type != const in logicaloperator.So string comparision is done.
    ERROR Feb 15 16:25:22 [14353]: type != const in logicaloperator.So string comparision is done.
    ERROR Feb 15 16:25:22 [14353]: type != const in logicaloperator.So string comparision is done.
    ERROR Feb 15 16:25:22 [14353]: Flag setting for this opcode is 16.
    INFO Feb 15 16:25:22 [14353]: Opcode response: status:500
    INFO Feb 15 16:25:22 [14353]: End SET Handler, Status : Success, Component : User, Transaction : 1, Operation : add.
    MESSAGE Feb 15 16:25:22 [14353]: ENTITY 'User' IMPORT Success
    INFO Feb 15 16:25:22 [14353]: Start Set Handler,Component : User
    ERROR Feb 15 16:25:22 [14353]: Key:ISCrEntity is not found in RequestMap File for User.
    ERROR Feb 15 16:25:22 [14353]: Parser Error: xmlvalue for jsonkey="description", xmlelement="/User/Description" cannot be found in request file.
    ERROR Feb 15 16:25:22 [14353]: Parser Error: xmlvalue for jsonkey="securitylevel", xmlelement="/User/UserType" cannot be found in request file.
    ERROR Feb 15 16:25:22 [14353]: Parser Error: xmlvalue for jsonkey="maclist", xmlelement="/User/MACAddressList/MACAddress" cannot be found in request file.
    ERROR Feb 15 16:25:22 [14353]: json object not found with key="securitylevel" to handle logicaloperator.
    ERROR Feb 15 16:25:22 [14353]: json object not found with key="securitylevel" to handle logicaloperator.
    ERROR Feb 15 16:25:22 [14353]: json object not found with key="securitylevel" to handle logicaloperator.
    ERROR Feb 15 16:25:22 [14353]: type != const in logicaloperator.So string comparision is done.
    ERROR Feb 15 16:25:22 [14353]: type != const in logicaloperator.So string comparision is done.
    ERROR Feb 15 16:25:22 [14353]: type != const in logicaloperator.So string comparision is done.
    ERROR Feb 15 16:25:22 [14353]: type != const in logicaloperator.So string comparision is done.
    ERROR Feb 15 16:25:22 [14353]: type != const in logicaloperator.So string comparision is done.
    ERROR Feb 15 16:25:22 [14353]: Flag setting for this opcode is 16.
    INFO Feb 15 16:25:22 [14353]: Opcode response: status:500
    INFO Feb 15 16:25:22 [14353]: End SET Handler, Status : Success, Component : User, Transaction : 2, Operation : add.
    MESSAGE Feb 15 16:25:22 [14353]: ENTITY 'User' IMPORT Success
    INFO Feb 15 16:25:22 [14353]: Command:/scripts/apiparser_generate_tar.sh /sdisk/api-1613402721773479.txt /sdisk/API-1613402721773479 /sdisk/APIXMLOutput/1613402721626.xml /sdisk/API-1613402721773479.tar /sdisk/API-1613402721773479.log 0 status:3
    INFO Feb 15 16:25:22 [14353]: No need to create Tar file. Response file is /sdisk/APIXMLOutput/1613402721626.xml

    And ofc no user imported...

    Thanks for your help.

  • Have a look at this https://community.sophos.com/xg-firewall/f/discussions/118992/create-ad-users-with-api

    Also use a decent text editor like notepad+++ and nothing with Microsoft connected to it.

    Also, it may be helpful to create a small say 2 user csv file, create the XML and post both here. It will be easier to debug. Suspect issue is with password.

    And in closing -- @Sophos -- stop torturing your users, bring back direct CSV import for users. The assertion that xml is more secure is nonsense as the passwords are just as plain in the XML.

  • Hi

    Unfortunately all those messages also appear in the apiparser.log when import is successful.

    One thing that can cause the 501 error response is if other objects referred to in the import data don't exist. For example, if the Group named in the csv line doesn't exist already, you will see a 501 response. 

    The example csv file uses 'Open Group' as the main group for the new users because it exists on all firewalls, but I wonder if that's true for firewalls that are installed in languages other than English.

    Is your firewall installed using a language other than English? Is there a group called 'Open Group' on your firewall or does it have a name in your local language? Try modifying the csv file to use a different group name that you know for sure exists.

  • Hi again,

    First of all thx for your quick reply ;o)

    Your are right the 'Open Group' name is 'Groupe Ouvert" in French Firewall but the problem is not there....

    have already used a real existing group name in the CSV called "Eleves" meaning Students

    this is the used CSV:

    Name,Username,Password,Email Address,Group
    Nigel Brown,nbrown,Pa5s!w0rd19,nigel.brown@example.com,Eleves
    Gina Lopez,glopez,e1Azjr8q9^21,gina.lopez@example.com,Eleves

    Have a nice day.

  • Hello When I lunch the code to export the userlist i get this error : 

    C:\Users\Boris Yenoa>pythonDownloads\userimport-py\UserImport.py -f 192.168.100.254 -i Documents\users.csv -u admin -p S&curity_Te@m2021 -a -n
    Reading from file: Documents\users.csv
    Config will be posted to 192.168.100.254
    Traceback (most recent call last):
    File "C:\Users\Boris Yenoa\pythonDownloads\userimport-py\UserImport.py", line 230, in <module>
    row["Password"],
    KeyError: 'Password'
    'curity_Te@m2021' n’est pas reconnu en tant que commande interne
    ou externe, un programme exécutable ou un fichier de commandes.

    C:\Users\Boris Yenoa>pythonDownloads\userimport-py\UserImport.py -f 192.168.100.254 -i Documents\users.csv -u admin -p S&curity_Te@m2021 -a
    Reading from file: Documents\users.csv
    Config will be posted to 192.168.100.254
    Traceback (most recent call last):
    File "C:\Users\Boris Yenoa\pythonDownloads\userimport-py\UserImport.py", line 230, in <module>
    row["Password"],
    KeyError: 'Password'
    'curity_Te@m2021' n’est pas reconnu en tant que commande interne
    ou externe, un programme exécutable ou un fichier de commandes.

    Somebody can help me?

  • Hello this continue to not work for me :

    C:\Users\Boris Yenoa>pythonDownloads\userimport-py\UserImport.py -f 192.168.100.254 -i Documents\users.csv -u admin -p S&curity_Te@m2021 -a -n
    Reading from file: Documents\users.csv
    Config will be posted to 192.168.100.254
    Read 25 users from file Documents\users.csv

    Sending XMLAPI request to 192.168.100.254:4444/.../APIController
    Traceback (most recent call last):
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\connectionpool.py", line 699, in urlopen
    httplib_response = self._make_request(
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\connectionpool.py", line 382, in _make_request
    self._validate_conn(conn)
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\connectionpool.py", line 1010, in _validate_conn
    conn.connect()
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\connection.py", line 411, in connect
    self.sock = ssl_wrap_socket(
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\util\ssl_.py", line 432, in ssl_wrap_socket
    ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls)
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\util\ssl_.py", line 474, in _ssl_wrap_socket_impl
    return ssl_context.wrap_socket(sock)
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\ssl.py", line 500, in wrap_socket
    return self.sslsocket_class._create(
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\ssl.py", line 1040, in _create
    self.do_handshake()
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\ssl.py", line 1309, in do_handshake
    self._sslobj.do_handshake()
    ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1123)

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\adapters.py", line 439, in send
    resp = conn.urlopen(
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\connectionpool.py", line 755, in urlopen
    retries = retries.increment(
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\urllib3\util\retry.py", line 573, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
    urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='192.168.100.254', port=4444): Max retries exceeded with url: /webconsole/APIController (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1123)')))

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File "C:\Users\Boris Yenoa\pythonDownloads\userimport-py\UserImport.py", line 239, in <module>
    result, r = xgAPIPost(fwRequestroot)
    File "C:\Users\Boris Yenoa\pythonDownloads\userimport-py\UserImport.py", line 179, in xgAPIPost
    r = requests.post(callurl, data=postdata, verify=stuff.insecure)
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\api.py", line 119, in post
    return request('post', url, data=data, json=json, **kwargs)
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\api.py", line 61, in request
    return session.request(method=method, url=url, **kwargs)
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\sessions.py", line 542, in request
    resp = self.send(prep, **send_kwargs)
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\sessions.py", line 655, in send
    r = adapter.send(request, **kwargs)
    File "C:\Users\Boris Yenoa\AppData\Local\Programs\Python\Python39\lib\site-packages\requests\adapters.py", line 514, in send
    raise SSLError(e, request=request)
    requests.exceptions.SSLError: HTTPSConnectionPool(host='192.168.100.254', port=4444): Max retries exceeded with url: /webconsole/APIController (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1123)')))
    'curity_Te@m2021' n’est pas reconnu en tant que commande interne
    ou externe, un programme exécutable ou un fichier de commandes.

  • Hi Boris,

    The first errors were because the password contains special characters that are being interpreted by the command-line interpreter. Try putting quotes around the password in the command-line.

    The second errors, regarding the cert chain, is addressed by adding '-n'. B ut because in your case the '-n' was after the password, the command line is interpreting it as part of a second command.

    So try this:

    pythonDownloads\userimport-py\UserImport.py -f 192.168.100.254 -i Documents\users.csv -u admin -p 'S&curity_Te@m2021' -a -n

    Regards

    Rich

  • Import Users with Powershell

    As a Mac user, I tend to forget that Python isn't there by default for the vast majority of our customers who work on Windows systems day-to-day.

    So here is a version of the script written for PowerShell. It should just work as-is without the need to install any additional software or dependencies.

    To run this script, first download it to a folder along with the csv file containing your user information for import. Open a Powershell command prompt window and run the command as follows:

    PS C:\Users\Bob\Downloads> .\UserImport.ps1 -fw 172.16.16.1 -infile users.csv

    Substitute the IP address or hostname of your firewall and the name of your csv file as appropriate.The script will prompt you for the password and attempt to authenticate with your firewall at the specified IP address as 'admin'.

    There are further command line options that are documented at the top of the code.

    This script also has the ability to translate object names if you've installed your firewall in a language other than English. Unfortunately this doesn't completely work and is waiting on a bug fix in an upcoming MR.

    # UserImport.ps1
    #
    # A Windows Powershell script to read user details from a CSV file and import 
    # them to a Sophos Firewall using the XML API
    #
    # Before using this script, you need to enable XML API access on your 
    # firewall for the IP address of the Windows computer that you're going 
    # to run it on.
    #
    # In the web admin console of your Firewall, go to 
    #        System > Backup & Firmware > API
    # Ensure that the 'Enabled' checkbox is set for API configuration and that 
    # the IP address of your Windows computer is in the 'Allowed IP address' list
    #
    # Run the script from the Windows Powershell prompt as follows:
    # .\UserImport.ps1 -infile <csv file name> -fw <target firewall>
    # 
    # Other parameters are optional:
    #  -username    Admin account name to use - defaults to 'admin'
    #  -password    Admin account password - a prompt will be shown if you don't 
    #               provide this at the command line
    #  -operation   Defaults to 'set' but 'update' can be used if modifying 
    #               existing user objects
    #  -lang        If your firewall has been reset with a default configuration 
    #               language other than English, specify the language here to use
    #               translated names for built-in policies and object.
    #               See the list of language codes below.
    #  -validate    If set, validate the TLS cert of the firewall. Defaults to
    #               not validating because SFOS uses a self-signed certificate
    #               by default.
    #
    
    param (
        [string]$infile, 
        [string]$fw = $null, 
        [string]$username='admin', 
        [string]$password = $( 
            if(-not [string]::IsNullOrEmpty($fw)) 
            { 
                $spw= Read-Host -AsSecureString "Password for" `
                                                "'$($username)' on $($fw)"
                [pscredential]::new('jpgr', $spw).GetNetworkCredential().Password
            } 
                    ), 
        [string]$operation='set', 
        [string]$lang="EN",
        [switch]$validate=$false
           )
    
    # Set up callback to ignore certificate validation because most Firewalls
    # run with the default self-signed certificates.
    
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    
        if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type)
        {
            $certCallback = @"
        using System;
        using System.Net;
        using System.Net.Security;
        using System.Security.Cryptography.X509Certificates;
        public class ServerCertificateValidationCallback
        {
            public static void Ignore()
            {
                if(ServicePointManager.ServerCertificateValidationCallback ==null)
                {
                    ServicePointManager.ServerCertificateValidationCallback += 
                        delegate
                        (
                            Object obj, 
                            X509Certificate certificate, 
                            X509Chain chain, 
                            SslPolicyErrors errors
                        )
                        {
                            return true;
                        };
                }
            }
            public static void UnIgnore()
            {
                ServicePointManager.ServerCertificateValidationCallback = null;
            }
        }
    "@
            Add-Type $certCallback
         }
    if ( $validate ) 
    {
        [ServerCertificateValidationCallback]::UnIgnore()
    } else {
        [ServerCertificateValidationCallback]::Ignore()
    }
    
    # Set up data and functions for later
    
    $langcode = @{
        "EN" = 0;		# English
        "DE" = 1;		# German
        "ES" = 2;		# Spanish
        "FR" = 3;		# French
        "IT" = 4;		# Italian
        "JP" = 5;		# Japanese
        "KR" = 6;		# Korean
        "PT" = 7;		# Portuguese (Brazilian)
        "RU" = 8;		# Russian
        "ZH-CN" = 9;    # Simplified Chinese
        "ZH-TW" = 10	# Traditional Chinese
    }
    
    $transobj = @{
        "Open Group" =
            "Open Group", "Offene Gruppe", "Grupo abierto",
             "Groupe ouvert", "Gruppo aperto", "オープングループ", "공개 그룹",
             "Grupo aberto", "Открытая группа", "开放组,開啟群組";
        "All the time" =
            "All the time", "Jederzeit", "Siempre", "Tout le temps",
             "Sempre", "常時", "항상", "O tempo todo", "Все время", "一直,隨時";
        "Allowed all the time" =
            "Allowed all the time", "Immer erlaubt", "Permitido siempre",
             "Toujours autorisé", "Sempre consentito", "常に許可", "항상 허용됨",
             "Sempre permitido", "Разрешены все время", "所有时间允许,任何時間都允許";
        "Unlimited Internet Access" =
             "Unlimited Internet Access", "Unbegrenzter Internetzugriff",
             "Acceso a Internet ilimitado", "Accès Internet illimité",
             "Accesso illimitato a Internet", "インターネットアクセスを制限しない",
             "무제한 인터넷 액세스", "Acesso à Internet ilimitado",
             "Безлимитный Интернет-доступ", "不受限制的互联网访问", "無限制網際網路存取"
    }
    
    $required=@("Name","Username","Password","Email Address","Group")
    
    # Function to convert strings that are translated on Firewalls that have been
    # switched to a different language.
    function Get-Translation {
        param ( [string]$phrase, [string]$lang )
        $output=$phrase
        if ($langcode.ContainsKey($lang))
        {
            if ($transobj.ContainsKey($phrase))
            {
                $output=$transobj.$phrase[$langcode.$lang]
            }
        } 
        $output
    }
    
    # Main stream of execution starts here
    # Check command-line parameters
    
    if ($operation -ne "set" -and $operation -ne 'update')
    {
        write-host "Invalid operation: Must be 'set' or 'update', not " `
                   "'$($operation)'"
        exit
    }
    
    
    if (-not $langcode.ContainsKey($lang))
    {
        write-host "Unsupported language: You entered '$($lang)'"
        write-host "Valid options: $($langcode.keys -join ', ')"
        exit
    }
    
    # Open the CSV file for reading - you might want to change some parameters 
    # here if your CSV file is non-standard
    
    $csv=Import-csv -path $infile -Encoding utf8
    
    # Check that the CSV file has the required columns
    $validCsv=$true
    $csvColumns=$csv[0].psobject.Properties.Name
    
    foreach($col in $required) 
    {
        if( -not ($csvColumns -contains $col) ) 
        {
            write-host "Missing column: $col"
            $validCsv=$false
        }
    }
    
    if( -not $validCsv)
    {
        write-host "CSV file $($infile) does not contain the required fields"
        write-host "Required columns are: $($required -join ", ")"
        write-host "Your file has: $($csvColumns -join ", ")"
        exit
    }
    
    # Now get a temp file name to write the XML API request file
    $xmlfile=[System.IO.Path]::GetTempFileName()
    #[string]$xmlfile="$(Get-Location)\out.xml"
    # write-host $xmlfile
    
    $xmlout = New-Object System.Xml.XmlTextWriter($xmlfile, $Null )
    
    $xmlout.Formatting = 'Indented'
    $xmlout.Indentation = 1
    
    # Write the login section of the XMLAPI call
    
    $xmlout.WriteStartDocument()
    $xmlout.WriteStartElement('Request')
    $xmlout.WriteStartElement('Login')
    $xmlout.WriteElementString('Username', $username)
    $xmlout.WriteElementString('Password', $password)
    $xmlout.WriteEndElement()
    
    # Now start the main 'Set' element
    
    $xmlout.WriteStartElement('Set')
    $xmlout.WriteAttributeString('operation', $operation)
    
    $serial=1
    $readItems=@{}
    
    # Loop through the CSV file line-by-line and generate XML for each line
    
    foreach($line in $csv)
    {
        $xmlout.WriteStartElement('User')
        $xmlout.WriteAttributeString('transactionid', $serial)
        $xmlout.WriteElementString('Username',$line.Username)
        $xmlout.WriteElementString('Name', $line.Name)
        $xmlout.WriteElementString('Password', $line.Password)
        $xmlout.WriteStartElement('EmailList')
        $xmlout.WriteElementString('EmailID', $line.'Email Address')
        $xmlout.WriteEndElement()
       
     
        $xmlout.WriteElementString('Group', 
                    ( Get-Translation -phrase $line.Group -lang $lang) )
       
        $xmlout.WriteElementString('SurfingQuotaPolicy', 
                    $transobj.'Unlimited Internet Access'[$langcode.$lang])
        $xmlout.WriteElementString('AccessTimePolicy', 
                    $transobj.'Allowed all the time'[$langcode.$lang])
        $xmlout.WriteElementString('DataTransferPolicy', "")
        $xmlout.WriteElementString('QoSPolicy',"")
        $xmlout.WriteElementString('SSLVPNPolicy',"")
        $xmlout.WriteElementString('Status',"Active")
        $xmlout.WriteElementString('L2TP',"Disable")
        $xmlout.WriteElementString('PPTP',"Disable")
        $xmlout.WriteElementString('CISCO',"Disable")
        $xmlout.WriteElementString('QuarantineDigest',"Disable")
        $xmlout.WriteElementString('MACBinding',"Disable")
        $xmlout.WriteElementString('LoginRestriction',"UserGroupNode")
        $xmlout.WriteElementString('ScheduleForApplianceAccess',
                    $transobj.'All the time'[$langcode.$lang])
        $xmlout.WriteElementString('LoginRestrictionForAppliance', "")
        $xmlout.WriteElementString('IsEncryptCert', "Disable")
        $xmlout.WriteElementString('SimultaneousLoginsGlobal', "Enable")
        
        $xmlout.WriteEndElement()
    
        # Remember key details for displaying when processing the API call result 
        $readItems.add($serial++, "$($line.Name) ($($line.Username))")
    }
    
    $xmlout.WriteEndElement()
    $xmlout.WriteEndDocument()
    $xmlout.Flush()
    $xmlout.Close()
    
    write-host "Read $($serial-1) users from $($infile)"
    write-host
    
    # Display the XML content on screen
    
    # Send the XML to the firewall if one was specified
    if (-not [string]::IsNullOrEmpty($fw)) {
      
    
        $uri="https://$($fw):4444/webconsole/APIController"
    
        write-host "Posting to $($uri)"
    
        $resp=Invoke-WebRequest -Uri $uri  -Method Post `
                          -Body @{"reqxml" = Get-Content -Path $xmlfile -Raw}
    
    # Check the response XML for information about the result
        if (-not [string]::IsNullOrEmpty($resp.Content)) 
        {
            Write-host "Returned $($resp.StatusCode) - $($resp.StatusDescription)"
            write-host $resp.Content
    
            [xml]$resxml=$resp.Content
            Select-Xml -Xml $resxml -XPath '/Response/User' | ForEach-Object {
                $thisid=[int]$_.node.transactionid
                write-host "$($thisid) - $($readItems.$thisid) : " `
                           "$($_.node.status.'#text') ($($_.node.status.code))"
            }
        }
    } 
    else 
    {
        write-host "No firewall specified - here's the XML I would have sent:"
        Get-Content -Encoding UTF8 $xmlfile 
    }
    
    # Clean up by deleting the generated XML
    
    Remove-Item $xmlfile
    

    Download as a zip



    Try again
    [edited by: RichBaldry at 11:18 PM (GMT -7) on 14 Mar 2021]
  • Hello,

    I downloaded the zip and ran the program as suggested. I am using a MacOS but am getting a Syntax error on line 37

    File "Downloads/UserImport.py", line 37
        print(*args, file=sys.stderr, **kwargs)
              ^
    SyntaxError: invalid syntax"

    My user.csv is formatted like so:

    Name,Username,Password,Email Address,Group
    atlantis sci0,sci0,sci0,sci0@atlantis.whoi.edu,SCIENCE
    Craig Young,sci1,Young,sci1@atlantis.whoi.edu,SCIENCE

    Please recommend on how to fix the syntax error.