Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Readme #19

Merged
merged 2 commits into from
Nov 29, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 134 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,137 @@
# Sophos Firewall Python SDK
This repository contains source code for a Sophos Firewall SDK that includes convenience methods for working with the Sophos Firewall XML API.
This repository contains the source code for the Sophos Firewall Python Software Development Kit (SDK).

For usage details please see the [documentation](https://super-adventure-p1qp7jm.pages.github.io/)
For installation and usage details please see the [documentation](https://super-adventure-p1qp7jm.pages.github.io/)

## Support
This code was developed by a small community of engineers within Sophos who will be maintaining the project. Questions can be posted to the [Q&A](https://github.com/sophos/sophos-firewall-sdk/discussions/categories/q-a) section of the Github project. If you are hitting a bug, please open a new [Issue](https://github.com/sophos/sophos-firewall-sdk/issues) 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.


## Contributing
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](https://github.com/sophos/sophos-firewall-sdk/pulls).

To be merged into the project, the following requirements must be met:
- Passing Pylint tests
- Code formatted with Black
- Unit tests written with Pytest

### Development
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](https://docs.sophos.com/nsg/sophos-firewall/18.0/API/index.html) describes how the API calls are structured.

#### GET Methods
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.

```python
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
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](https://jinja.palletsprojects.com/en/3.1.x/templates/#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.

```xml
<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.

```python
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
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.

```python
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
```

#### Tests
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.
Loading