The Sophos Firewall Python Software Development Kit (SDK) provides a Python module for working with the XML API of Sophos Firewall.
For installation and usage details please see the documentation
The Sophos Firewall Python SDK was developed by a small community of engineers within Sophos who will be maintaining the project. Questions can be posted to the Q&A section of the Github project. If you are hitting a bug, please open a new Issue and fill out the Bug Report template. If you would like to see a new feature implemented, please fill out the Feature Request template.
This project will utilize a community support model as outlined above. Support will not be provided by Sophos Technical Support.
We welcome contributors to the project. To work on a new feature, fork the project and then develop your feature within the fork. When the new feature is ready for review, please submit a Pull Request.
To be merged into the project, the following requirements must be met:
- Passing Pylint tests
- Code formatted with Black
- Unit tests written with Pytest
The main code to be updated when developing new features is inside of the SophosFirewall
class, which is located in the firewallapi.py
module. Functionality is implemented by defining methods under the class. There are currently three types of methods: GET
, CREATE
, and UPDATE
. The API Documentation describes how the API calls are structured.
GET methods provide retrieval of existing firewall configuration settings. They are implemented by calling the get_tag
and/or the get_tag_with_filter
methods of the class. The get_tag
method requests a specified XML tag from the API and returns the content as a Python dict
object. The get_tag
will return all objects for the specified XML tag. The get_tag_with_filter
method requests an XML tag from the API with specified filter criteria such as =
, !=
, or like
. This allows for filtering of the returned data. Below is an example of a GET method that requests IP Hosts from the firewall using the IPHost
XML tag. It utilizes both the get_tag
and the get_tag_with_filter
class methods, depending on the options provided to the method. If the calling program specifies no options, then all records are returned using the get_tag
method. Otherwise, the results can be filtered by specifying the name or IP address. When name or IP address are provided, the get_tag_with_filter
method is used to filter the results.
def get_ip_host(
self, name: str = None, ip_address: str = None, operator: str = "="):
"""Get IP Host object(s)
Args:
name (str, optional): IP object name. Returns all objects if not specified.
ip_address (str, optional): Query by IP Address.
operator (str, optional): Operator for search. Default is "=". Valid operators: =, !=, like.
"""
if name:
return self.get_tag_with_filter(
xml_tag="IPHost", key="Name", value=name, operator=operator
)
if ip_address:
return self.get_tag_with_filter(
xml_tag="IPHost",
key="IPAddress",
value=ip_address,
operator=operator,
)
return self.get_tag(xml_tag="IPHost")
Create methods are used to create new configuration objects on the firewall. To define a new Create method, the XML payload for the request must first be created in a template and stored in the templates
directory. The XML payload in the template can contain Jinja variables that will be populated when the template is rendered. Below is an example template containing the XML payload to create an IP Host. In the template, the values within the double brackets {{ var }}
are variables that are passed in by the create method arguments.
<Request>
<Login>
<Username>{{username}}</Username>
<Password >{{password}}</Password>
</Login>
<Set operation="add">
<IPHost transactionid="">
<Name>{{ name }}</Name>
<IPFamily>IPv4</IPFamily>
<HostType>IP</HostType>
<IPAddress>{{ ip_address }}</IPAddress>
</IPHost>
</Set>
</Request>
Once the template is defined, a new create method can be written using the submit_template
method. The submit_template
method will render the template, passing in any variables that are specified in the new create method. It will then issue a POST
request against the firewall API to create the new object.
Below is the method to create an IP Host. The source program must specify the name
and ip_address
as arguments when calling the function. These values are then populated in a dictionary called params
and are passed to the submit_template
method. There is also an optional debug
argument which causes the API response to be output to screen for troubleshooting purposes.
def create_ip_host(
self, name: str, ip_address: str, debug: bool = False
):
"""Create IP address object
Args:
name (str): Name of the object
ip_address (str): Host IP address
debug (bool, optional): Turn on debugging. Defaults to False.
Returns:
dict: XML response converted to Python dictionary
"""
self._validate_ip_address(ip_address)
params = {"name": name, "ip_address": ip_address}
resp = self.submit_template(
"createiphost.j2", template_vars=params, debug=debug
)
return resp
Update methods provide the ability to change existing configuration on the firewall. When defining update methods, it is often necessary to first do a GET request to retrieve the existing configuration. This is because often the existing configuration must be in the payload in addition to any modifications. For example, when updating a URL Group with a new entry, the list must contain the existing entries along with the new one. Otherwise, the list will only contain the new entry when updated. Update methods can therefore first use an existing get method if defined, or can use the get_tag
and/or get_tag_with_filter
methods. Then, the information from the get request can be parsed and modified as necessary. Finally, the template can be submitted with the submit_template
method, using the modified variables from the get request when rendering the template.
Below is the code to update a URL Group on the firewall. It first does a get request using the existing get_urlgroup
method. It parses the existing list of URLs from the response, and then adds the new domain to the list. Finally, it uses the submit_template
method to submit the updateurlgroup.j2
template, passing in as variables the name of the list to be updated and the updated domain list.
def update_urlgroup(
self, name: str, domain: str, debug: bool = False
):
"""Adds a specified domain to a web URL Group
Args:
name (str): URL Group name
domain (str): Domain to be added to URL Group
debug (bool, optional): Enable debug mode. Defaults to False.
Returns:
dict: XML response converted to Python dictionary
"""
# Get the existing URL list first, if any
resp = self.get_urlgroup(name=name)
if "URLlist" in resp["Response"]["WebFilterURLGroup"]:
exist_list = (
resp.get("Response").get("WebFilterURLGroup").get("URLlist").get("URL")
)
else:
exist_list = None
domain_list = []
if exist_list:
if isinstance(exist_list, str):
domain_list.append(exist_list)
elif isinstance(exist_list, list):
domain_list = exist_list
domain_list.append(domain)
params = {"name": name, "domain_list": domain_list}
resp = self.submit_template(
"updateurlgroup.j2", template_vars=params, debug=debug
)
return resp
Pytest test cases are in the unittests.py
module. The tests are executed by Github Actions on commits to the develop
and/or main
branch. It is not required to create new tests for Get methods, but it is recommended for Create or Update methods. Success and failure cases should be covered. The existing tests utilize the methods under test with sample input data and compare against a mocked API response. Please see the test_create_rule
and test_failed_create_rule
tests in the unittests.py
for reference.