├── .github └── workflows │ ├── docs.yml │ ├── python-publish.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── docs ├── Makefile ├── _static │ └── firepyer-logo.png ├── change.rst ├── conf.py ├── deployments.rst ├── fp_output │ ├── create_access_rule_params.txt │ ├── create_icmp_port_params.txt │ ├── create_net_group_params.txt │ ├── create_network_params.txt │ ├── create_port_group_params.txt │ ├── create_port_object_params.txt │ ├── create_syslog_server_params.txt │ ├── deploy_now.txt │ ├── expand_merged_csv_params.txt │ ├── export_config_params.txt │ ├── get_access_rules.txt │ ├── get_bgp_general_settings.txt │ ├── get_bgp_settings.txt │ ├── get_config_files.txt │ ├── get_deployment_status_params.txt │ ├── get_dhcp_servers.txt │ ├── get_icmp_ports.txt │ ├── get_icmp_ports_params.txt │ ├── get_intrusion_policies.txt │ ├── get_net_groups.txt │ ├── get_net_groups_params.txt │ ├── get_net_obj_or_grp_params.txt │ ├── get_net_objects.txt │ ├── get_net_objects_params.txt │ ├── get_ospf_settings.txt │ ├── get_pending_changes.txt │ ├── get_port_groups.txt │ ├── get_port_obj_or_grp_params.txt │ ├── get_syslog_servers.txt │ ├── get_system_info.txt │ ├── get_upgrade_files.txt │ ├── get_vrfs.txt │ ├── read_groups_csv_params.txt │ ├── read_objects_csv_params.txt │ ├── send_command_params.txt │ ├── set_bgp_general_settings_params.txt │ ├── update_intrusion_rules.txt │ ├── upload_config_params.txt │ └── upload_vdb_file_params.txt ├── fpoutput.py ├── index.rst ├── install.rst ├── interfaces.rst ├── make.bat ├── methods ├── networks.rst ├── policies.rst ├── ports.rst ├── routing.rst ├── system.rst ├── using.rst └── utils.rst ├── firepyer ├── __init__.py ├── exceptions.py └── utils.py ├── requirements.txt ├── setup.py └── tests ├── test_fdm.py ├── test_network.py └── test_system.py /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Build 15 | uses: ammaraskar/sphinx-action@master 16 | with: 17 | pre-build-command: python -mpip install sphinx-rtd-theme && python -mpip install -r requirements.txt 18 | docs-folder: "docs/" 19 | 20 | - name: Deploy 21 | uses: peaceiris/actions-gh-pages@v3 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | publish_dir: ./docs/_build/html 25 | force_orphan: true 26 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install -r requirements.txt 19 | - name: Run unit tests 20 | run: python -m unittest discover -s tests 21 | 22 | deploy: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Set up Python 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: '3.x' 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install setuptools wheel twine 34 | - name: Build and publish 35 | env: 36 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 37 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 38 | run: | 39 | python setup.py sdist bdist_wheel 40 | twine upload dist/* 41 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: 3 | push: 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Install dependencies 11 | run: | 12 | python -m pip install --upgrade pip 13 | pip install -r requirements.txt 14 | - name: Run tests with pytest 15 | run: python -m unittest discover -s tests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | *.pyc 3 | .vscode/ 4 | docs/_build 5 | build 6 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Firepyer 2 | Copyright 2021 Certa Networks Limited, licensed under Apache License 2.0. All rights reserved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Firepyer](docs/_static/firepyer-logo.png "Firepyer logo") 2 | 3 | Firepyer provides a way of interacting with Cisco Firepower devices via their REST APIs in Python. Currently FTD devices using FDM (not FMC) are supported. 4 | The intended usage is to replace some of the tedious clicking tasks from the GUI, perform actions on a large number of devices or execute bulk imports of objects, rules etc. 5 | 6 | The following versions have been used in development (others should work but YMMV): 7 | - Python 3.9 (3.8+ is required) 8 | - FTD 6.6.1-91 9 | 10 | Please see the brief instructions below on installing and using Firepyer and visit [the documentation](https://certanet.github.io/firepyer/) for a more comprehensive guide and examples. 11 | 12 | 13 | ## Installation 14 | 15 | The latest release is available to download from PyPI, simply using `pip install firepyer`. 16 | 17 | Alternatively, as this project is still in early development, the best place to get the most recent features is directly from the [source GitHub repo](https://github.com/certanet/firepyer). 18 | 19 | ## Usage 20 | 21 | All functionailty for interacting with an FTD device is contained within the Fdm class and it’s methods. Authentication is taken care of transparently when calling a method, so this doesn’t need to be done explicilty. 22 | 23 | Import the Fdm class and instantiate an object, passing in your FTD hostname/IP, username and password (and ignoring SSL verification if using an untrusted/self-signed cert): 24 | 25 | >>> from firepyer import Fdm 26 | >>> fdm = Fdm(host='192.168.45.45', username='admin', password='Admin123', verify=False) 27 | 28 | Then call any of the available methods to run against your FTD: 29 | 30 | >>> fdm.get_hostname() 31 | 'firepyer2120' 32 | 33 | >>> fdm.get_net_objects('any-ipv4') 34 | {'description': None, 35 | 'dnsResolution': None, 36 | 'id': '00f7b297-4d44-11eb-9e04-13721b05d633', 37 | 'isSystemDefined': True, 38 | 'links': {'self': 'https://192.168.45.45/api/fdm/latest/object/networks/00f7b297-4d44-11eb-9e04-13721b05d633'}, 39 | 'name': 'any-ipv4', 40 | 'subType': 'NETWORK', 41 | 'type': 'networkobject', 42 | 'value': '0.0.0.0/0', 43 | 'version': 'kxd2dzxm2gtwn'} 44 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/firepyer-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certanet/firepyer/772093a7c8904e100d099492d2432ef229ec426a/docs/_static/firepyer-logo.png -------------------------------------------------------------------------------- /docs/change.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | * 0.0.7 - 2022-02-06 5 | * Fixed issue instantiating Fdm 6 | * Added tests 7 | 8 | * 0.0.6 - 2022-01-13 9 | * Fixed issue uploading update file in Windows enviornments 10 | 11 | * 0.0.5 - 2021-09-11 12 | * Fixed deleting config file 13 | * Changed no pending changes to return True 14 | * Added auto deploy to config import 15 | * Added PLR license methods 16 | * Added method to change admin password 17 | 18 | * 0.0.4 - 2021-03-13 19 | * Added ability to upload, import, export and download JSON configs 20 | * Added methods to update intrusion rules and geolocation and vulnerability databases 21 | * Added methods to upload and view system upgrade files 22 | * Added ability to delete network groups/objects and access rules 23 | 24 | * 0.0.3 - 2021-02-27 25 | * Create methods now return the created object 26 | * Added OSPF and sub-interface methods 27 | * Fixed issues accessing certain API endpoints 28 | 29 | * 0.0.2 - 2021-01-12 30 | * Added system, security policy and ICMP methods 31 | 32 | * 0.0.1 - 2021-01-09 33 | * Initial release -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | sys.path.insert(0, os.path.abspath('.')) 17 | 18 | from firepyer import __version__ 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'Firepyer' 24 | copyright = '2021, Marcus Cockerill, Certa Networks' 25 | author = 'Marcus Cockerill' 26 | 27 | # The short X.Y version. 28 | version = __version__ 29 | # The full version, including alpha/beta/rc tags 30 | release = __version__ 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = ['sphinx.ext.autodoc', 'fpoutput'] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 47 | 48 | # The name of the Pygments (syntax highlighting) style to use. 49 | pygments_style = 'sphinx' 50 | 51 | # -- Options for HTML output ------------------------------------------------- 52 | 53 | # The theme to use for HTML and HTML Help pages. See the documentation for 54 | # a list of builtin themes. 55 | # 56 | html_theme = 'sphinx_rtd_theme' 57 | 58 | # The name of an image file (relative to this directory) to place at the top 59 | # of the sidebar. 60 | html_logo = '_static/firepyer-logo.png' 61 | 62 | # Add any paths that contain custom static files (such as style sheets) here, 63 | # relative to this directory. They are copied after the builtin static files, 64 | # so a file named "default.css" will overwrite the builtin "default.css". 65 | html_static_path = ['_static'] 66 | 67 | # If true, links to the reST sources are added to the pages. 68 | html_show_sourcelink = False 69 | 70 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 71 | html_show_sphinx = False 72 | 73 | autoclass_content = 'both' -------------------------------------------------------------------------------- /docs/deployments.rst: -------------------------------------------------------------------------------- 1 | Deployments 2 | ============= 3 | 4 | FTD configuration changes need to be deployed in order to take effect. 5 | Pending changes can be viewed, discarded or commited via the API. 6 | 7 | 8 | .. py:currentmodule:: firepyer 9 | 10 | .. class:: Fdm 11 | :noindex: 12 | 13 | .. automethod:: deploy_config 14 | .. automethod:: deploy_now 15 | .. fp_output:: deploy_now 16 | .. automethod:: get_deployment_status 17 | .. fp_output:: get_deployment_status_params 18 | .. automethod:: get_pending_changes 19 | .. fp_output:: get_pending_changes 20 | -------------------------------------------------------------------------------- /docs/fp_output/create_access_rule_params.txt: -------------------------------------------------------------------------------- 1 | 'my-http-rule', 'PERMIT', src_zones=['inside_zone'], dst_zones=['outside_zone'], src_networks=['Host1-NIC1'], dst_networks=['GROUP-ALL-HOSTS'], dst_ports=['HTTP'], int_policy='Maximum Detection', log='both' 2 | 3 | {'destinationDynamicObjects': [], 4 | 'destinationNetworks': [{'id': '2904a9b9-4db6-11eb-aab5-93170f9c3b34', 5 | 'name': 'GROUP-ALL-HOSTS', 6 | 'type': 'networkobjectgroup', 7 | 'version': '3zkyarvfx3qx'}], 8 | 'destinationPorts': [{'id': '18312adc-38bb-11e2-86aa-62f0c593a59a', 9 | 'name': 'HTTP', 10 | 'type': 'tcpportobject', 11 | 'version': 'jfkuxugpghogc'}], 12 | 'destinationZones': [{'id': 'b1af33e1-b3e5-11e5-8db8-afdc0be5453e', 13 | 'name': 'outside_zone', 14 | 'type': 'securityzone', 15 | 'version': 'chx6737ygiktz'}], 16 | 'embeddedAppFilter': None, 17 | 'eventLogAction': 'LOG_BOTH', 18 | 'filePolicy': None, 19 | 'id': '8dfa08d4-7945-11eb-b948-03b19829b466', 20 | 'identitySources': [], 21 | 'intrusionPolicy': {'id': '80875699-4d44-11eb-9e04-190e1a7b8344', 22 | 'name': 'Maximum Detection', 23 | 'type': 'intrusionpolicy', 24 | 'version': 'nlypnji4gaseu'}, 25 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/policy/accesspolicies/c78e66bc-cb57-43fe-bcbf-96b79b3475b3/accessrules/8dfa08d4-7945-11eb-b948-03b19829b466'}, 26 | 'logFiles': False, 27 | 'name': 'my-http-rule', 28 | 'ruleAction': 'PERMIT', 29 | 'ruleId': 268435507, 30 | 'sourceDynamicObjects': [], 31 | 'sourceNetworks': [{'id': '9f42dab3-4db5-11eb-aab5-19160f885ae8', 32 | 'name': 'Host1-NIC1', 33 | 'type': 'networkobject', 34 | 'version': 'jpfwstwwapru5'}], 35 | 'sourcePorts': [], 36 | 'sourceZones': [{'id': '90c377e0-b3e5-11e5-8db8-651556da7898', 37 | 'name': 'inside_zone', 38 | 'type': 'securityzone', 39 | 'version': 'm6c22ydlemewj'}], 40 | 'syslogServer': None, 41 | 'timeRangeObjects': [], 42 | 'type': 'accessrule', 43 | 'urlFilter': None, 44 | 'users': [], 45 | 'version': 'kazwzgi6htvr5'} -------------------------------------------------------------------------------- /docs/fp_output/create_icmp_port_params.txt: -------------------------------------------------------------------------------- 1 | name='ping-reply', type='ECHO_REPLY' 2 | 3 | {'description': None, 4 | 'icmpv4Code': None, 5 | 'icmpv4Type': 'ECHO_REPLY', 6 | 'id': '32bc6efd-794b-11eb-b948-a194e41e088a', 7 | 'isSystemDefined': False, 8 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/icmpv4ports/32bc6efd-794b-11eb-b948-a194e41e088a'}, 9 | 'name': 'ping-reply', 10 | 'type': 'icmpv4portobject', 11 | 'version': 'ficvgobxazqtj'} -------------------------------------------------------------------------------- /docs/fp_output/create_net_group_params.txt: -------------------------------------------------------------------------------- 1 | name='GROUP-HOST1', description='GROUP-HOST1', objects=['Host1-NIC1', 'Host1-NIC2'] 2 | 3 | {'description': 'GROUP-HOST1', 4 | 'id': '26dbfd53-4db6-11eb-aab5-4dba63992bb3', 5 | 'isSystemDefined': False, 6 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networkgroups/26dbfd53-4db6-11eb-aab5-4dba63992bb3'}, 7 | 'name': 'GROUP-HOST1', 8 | 'objects': [{'id': '9f42dab3-4db5-11eb-aab5-19160f885ae8', 9 | 'name': 'Host1-NIC1', 10 | 'type': 'networkobject', 11 | 'version': 'jpfwstwwapru5'}, 12 | {'id': '9ffc3fa6-4db5-11eb-aab5-23e5ffdb3f0d', 13 | 'name': 'Host1-NIC2', 14 | 'type': 'networkobject', 15 | 'version': 'joq4kyvc4ztx5'}], 16 | 'type': 'networkobjectgroup', 17 | 'version': 'lcplbsj24ebva'} -------------------------------------------------------------------------------- /docs/fp_output/create_network_params.txt: -------------------------------------------------------------------------------- 1 | name='RFC-1918-172', value='172.16.0.0/12', type='network' 2 | 3 | {'description': None, 4 | 'dnsResolution': 'IPV4_ONLY', 5 | 'id': 'f8b81657-793a-11eb-b948-fbbf2c1ae1be', 6 | 'isSystemDefined': False, 7 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networks/f8b81657-793a-11eb-b948-fbbf2c1ae1be'}, 8 | 'name': 'RFC-1918-172', 9 | 'subType': 'NETWORK', 10 | 'type': 'networkobject', 11 | 'value': '172.16.0.0/12', 12 | 'version': 'oani42zndwyl5'} -------------------------------------------------------------------------------- /docs/fp_output/create_port_group_params.txt: -------------------------------------------------------------------------------- 1 | name='HTTP-S Group', objects=['HTTP', 'HTTPS'], description='HTTP and HTTPS ports' 2 | 3 | {'description': 'HTTP and HTTPS ports', 4 | 'id': 'cf42834b-794c-11eb-b948-efc7c141f6b3', 5 | 'isSystemDefined': False, 6 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/portgroups/cf42834b-794c-11eb-b948-efc7c141f6b3'}, 7 | 'name': 'HTTP-S Group', 8 | 'objects': [{'id': '18312adc-38bb-11e2-86aa-62f0c593a59a', 9 | 'name': 'HTTP', 10 | 'type': 'tcpportobject', 11 | 'version': 'jfkuxugpghogc'}, 12 | {'id': '1834bd00-38bb-11e2-86aa-62f0c593a59a', 13 | 'name': 'HTTPS', 14 | 'type': 'tcpportobject', 15 | 'version': 'f7j76od54tkia'}], 16 | 'type': 'portobjectgroup', 17 | 'version': 'cwappx23fqvaz'} -------------------------------------------------------------------------------- /docs/fp_output/create_port_object_params.txt: -------------------------------------------------------------------------------- 1 | name='my-http-port', port='8001', type='tcp', description='My custom HTTP port 8001' 2 | 3 | {'description': 'My custom HTTP port 8001', 4 | 'id': 'e10fd062-794b-11eb-b948-b5d9fac6117d', 5 | 'isSystemDefined': False, 6 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/tcpports/e10fd062-794b-11eb-b948-b5d9fac6117d'}, 7 | 'name': 'my-http-port', 8 | 'port': '8001', 9 | 'type': 'tcpportobject', 10 | 'version': 'bb4wded6dz4zt'} -------------------------------------------------------------------------------- /docs/fp_output/create_syslog_server_params.txt: -------------------------------------------------------------------------------- 1 | ip='10.1.5.14' 2 | 3 | {'deviceInterface': None, 4 | 'host': '10.1.5.14', 5 | 'id': '0fe4932c-794e-11eb-b948-27bb3f4588ef', 6 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/syslogalerts/0fe4932c-794e-11eb-b948-27bb3f4588ef'}, 7 | 'name': '10.1.5.14:514', 8 | 'port': '514', 9 | 'protocol': 'UDP', 10 | 'type': 'syslogserver', 11 | 'useManagementInterface': True, 12 | 'version': 'buzrvvrcvt6ly'} -------------------------------------------------------------------------------- /docs/fp_output/deploy_now.txt: -------------------------------------------------------------------------------- 1 | 'a4cacb2a-5102-11eb-aab5-9f6954baf492' -------------------------------------------------------------------------------- /docs/fp_output/expand_merged_csv_params.txt: -------------------------------------------------------------------------------- 1 | 'raw_network_groups.csv' 2 | 3 | [{'description': 'Host1', 4 | 'name': 'GROUP-HOST1', 5 | 'objects': 'Host1-NIC1'}, 6 | {'description': 'Host1', 7 | 'name': 'GROUP-HOST1', 8 | 'objects': 'Host1-NIC2'}, 9 | {'description': 'Host2', 10 | 'name': 'GROUP-HOST2', 11 | 'objects': 'Host2-NIC1'}, 12 | {'description': 'Host2', 13 | 'name': 'GROUP-HOST2', 14 | 'objects': 'Host2-NIC2'}, 15 | {'description': 'All hosts', 16 | 'name': 'GROUP-ALL-HOSTS', 17 | 'objects': 'GROUP-HOST1'}, 18 | {'description': 'All hosts', 19 | 'name': 'GROUP-ALL-HOSTS', 20 | 'objects': 'GROUP-HOST2'}] -------------------------------------------------------------------------------- /docs/fp_output/export_config_params.txt: -------------------------------------------------------------------------------- 1 | 'config-with-ospf.zip' 2 | 3 | {'configExportType': 'FULL_EXPORT', 4 | 'deployedObjectsOnly': True, 5 | 'diskFileName': 'config-with-ospf.zip', 6 | 'doNotEncrypt': True, 7 | 'encryptionKey': None, 8 | 'entityIds': None, 9 | 'forceOperation': False, 10 | 'id': '264f7ef6-8431-11eb-ab2b-31e0e0bf5eef', 11 | 'ipAddress': '192.168.133.100', 12 | 'jobHistoryUuid': '266020c7-8431-11eb-ab2b-41e10f130001', 13 | 'jobName': 'Config Export', 14 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/action/configexport/264f7ef6-8431-11eb-ab2b-31e0e0bf5eef'}, 15 | 'scheduleType': 'IMMEDIATE', 16 | 'type': 'scheduleconfigexport', 17 | 'user': 'admin', 18 | 'version': 'i7flijevvkqzu'} -------------------------------------------------------------------------------- /docs/fp_output/get_access_rules.txt: -------------------------------------------------------------------------------- 1 | [{'destinationDynamicObjects': [], 2 | 'destinationNetworks': [{'id': '2904a9b9-4db6-11eb-aab5-93170f9c3b34', 3 | 'name': 'GROUP-ALL-HOSTS', 4 | 'type': 'networkobjectgroup', 5 | 'version': 'i4oe7b4lpaxos'}], 6 | 'destinationPorts': [{'id': '18312adc-38bb-11e2-86aa-62f0c593a59a', 7 | 'name': 'HTTP', 8 | 'type': 'tcpportobject', 9 | 'version': 'jfkuxugpghogc'}], 10 | 'destinationZones': [{'id': 'b1af33e1-b3e5-11e5-8db8-afdc0be5453e', 11 | 'name': 'outside_zone', 12 | 'type': 'securityzone', 13 | 'version': 'chx6737ygiktz'}], 14 | 'embeddedAppFilter': None, 15 | 'eventLogAction': 'LOG_FLOW_END', 16 | 'filePolicy': None, 17 | 'id': '5c405b75-5105-11eb-aab5-0140ccd4feb7', 18 | 'identitySources': [], 19 | 'intrusionPolicy': {'id': '80875699-4d44-11eb-9e04-190e1a7b8344', 20 | 'name': 'Maximum Detection', 21 | 'type': 'intrusionpolicy', 22 | 'version': 'nlypnji4gaseu'}, 23 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/policy/accesspolicies/c78e66bc-cb57-43fe-bcbf-96b79b3475b3/accessrules/5c405b75-5105-11eb-aab5-0140ccd4feb7'}, 24 | 'logFiles': False, 25 | 'name': 'very-secure-rule', 26 | 'ruleAction': 'PERMIT', 27 | 'ruleId': 268435505, 28 | 'sourceDynamicObjects': [], 29 | 'sourceNetworks': [{'id': '9f42dab3-4db5-11eb-aab5-19160f885ae8', 30 | 'name': 'Host1-NIC1', 31 | 'type': 'networkobject', 32 | 'version': 'jpfwstwwapru5'}], 33 | 'sourcePorts': [], 34 | 'sourceZones': [{'id': '90c377e0-b3e5-11e5-8db8-651556da7898', 35 | 'name': 'inside_zone', 36 | 'type': 'securityzone', 37 | 'version': 'm6c22ydlemewj'}], 38 | 'syslogServer': None, 39 | 'timeRangeObjects': [], 40 | 'type': 'accessrule', 41 | 'urlFilter': {'type': 'embeddedurlfilter', 42 | 'urlCategories': [], 43 | 'urlObjects': []}, 44 | 'users': [], 45 | 'version': 'hjxr5d7i3mg5s'}] -------------------------------------------------------------------------------- /docs/fp_output/get_bgp_general_settings.txt: -------------------------------------------------------------------------------- 1 | {'aggregateTimer': 30, 2 | 'asNumber': '65500', 3 | 'asnotationDot': False, 4 | 'bgpBestPath': {'alwaysCompareMed': False, 5 | 'bestPathCompareRouterId': False, 6 | 'bestPathMedMissingAsWorst': False, 7 | 'defaultLocalPreferenceValue': 100, 8 | 'deterministicMed': False, 9 | 'type': 'bgpbestpath'}, 10 | 'bgpGracefulRestart': None, 11 | 'bgpNextHopTriggerDelay': 5, 12 | 'bgpNextHopTriggerEnable': True, 13 | 'bgpTimers': {'holdTime': 180, 14 | 'keepAlive': 60, 15 | 'minHoldTime': 0, 16 | 'type': 'bgptimers'}, 17 | 'description': None, 18 | 'enforceFirstAs': True, 19 | 'fastExternalFallOver': True, 20 | 'id': '12345678-1234-1234-1234-123456789abc', 21 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/devices/default/routing/bgpgeneralsettings/12345678-1234-1234-1234-123456789abc'}, 22 | 'logNeighborChanges': True, 23 | 'maxasLimit': None, 24 | 'name': 'BgpGeneralSettings', 25 | 'routerId': None, 26 | 'scanTime': 60, 27 | 'transportPathMtuDiscovery': True, 28 | 'type': 'bgpgeneralsettings', 29 | 'version': 'omg123ffsabcd'} -------------------------------------------------------------------------------- /docs/fp_output/get_bgp_settings.txt: -------------------------------------------------------------------------------- 1 | {'addressFamilyIPv4': {'afTableMap': None, 2 | 'aggregateAddressesIPv4s': [], 3 | 'aggregateTimer': 30, 4 | 'autoSummary': False, 5 | 'bgpNextHopTriggerDelay': 5, 6 | 'bgpNextHopTriggerEnable': True, 7 | 'bgpRedistributeInternal': False, 8 | 'bgpSupressInactive': False, 9 | 'defaultInformationOrginate': False, 10 | 'distance': {'externalDistance': 20, 11 | 'internalDistance': 200, 12 | 'localDistance': 200, 13 | 'type': 'afbgpdistance'}, 14 | 'distributeLists': [], 15 | 'injectMaps': [], 16 | 'maximumPaths': None, 17 | 'neighbors': [], 18 | 'networks': [], 19 | 'redistributeProtocols': [], 20 | 'scanTime': 60, 21 | 'synchronization': False, 22 | 'type': 'afipv4'}, 23 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/devices/default/routing/virtualrouters/12345678-1234-1234-1234-123456789abc/bgp/12345678-1234-1234-1234-123456789abc'}, 24 | 'name': 'bgp-general', 25 | 'routerId': None, 26 | 'type': 'bgp', 27 | 'version': 'omg123ffsabcd'} -------------------------------------------------------------------------------- /docs/fp_output/get_config_files.txt: -------------------------------------------------------------------------------- 1 | [{'dateModified': '2021-03-13 19:20:26Z', 2 | 'diskFileName': 'config-with-ospf.zip', 3 | 'id': 'default', 4 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/action/configfiles/default'}, 5 | 'sizeBytes': 11374, 6 | 'type': 'configimportexportfileinfo'}, 7 | {'dateModified': '2021-03-13 16:24:10Z', 8 | 'diskFileName': 'full_config-1.txt', 9 | 'id': 'default', 10 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/action/configfiles/default'}, 11 | 'sizeBytes': 69658, 12 | 'type': 'configimportexportfileinfo'}] -------------------------------------------------------------------------------- /docs/fp_output/get_deployment_status_params.txt: -------------------------------------------------------------------------------- 1 | 'a4cacb2a-5102-11eb-aab5-9f6954baf492' 2 | 3 | 'DEPLOYED' -------------------------------------------------------------------------------- /docs/fp_output/get_dhcp_servers.txt: -------------------------------------------------------------------------------- 1 | {'autoConfig': True, 2 | 'id': 'a47ce00c-fe55-11e4-8e99-f73968181bfd', 3 | 'interface': {'hardwareName': 'GigabitEthernet0/0', 4 | 'id': '8d6c41df-3e5f-465b-8e5a-d336b282f93f', 5 | 'name': 'outside', 6 | 'type': 'physicalinterface', 7 | 'version': 'h4kqp4iu2yvff'}, 8 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/devicesettings/default/dhcpservercontainers/a47ce00c-fe55-11e4-8e99-f73968181bfd'}, 9 | 'name': 'DHCP-Server-Container', 10 | 'primaryDNS': None, 11 | 'primaryWINS': None, 12 | 'secondaryDNS': None, 13 | 'secondaryWINS': None, 14 | 'servers': [{'addressPool': '192.168.45.46-192.168.45.254', 15 | 'enableDHCP': True, 16 | 'interface': {'hardwareName': 'GigabitEthernet0/1', 17 | 'id': 'ad6a9497-4d44-11eb-9e04-63d0b1958967', 18 | 'name': 'inside', 19 | 'type': 'physicalinterface', 20 | 'version': 'eqotynhtlcuyf'}, 21 | 'type': 'dhcpserver'}, 22 | {'addressPool': '192.168.133.8-192.168.133.100', 23 | 'enableDHCP': False, 24 | 'interface': {'hardwareName': 'GigabitEthernet0/2', 25 | 'id': 'aeb5b238-4d44-11eb-9e04-cd44159d2943', 26 | 'name': 'dmz', 27 | 'type': 'physicalinterface', 28 | 'version': 'ojwiwyovklamk'}, 29 | 'type': 'dhcpserver'}], 30 | 'type': 'dhcpservercontainer', 31 | 'version': 'eb6ciywtkaqs4'} -------------------------------------------------------------------------------- /docs/fp_output/get_icmp_ports.txt: -------------------------------------------------------------------------------- 1 | [{'description': None, 2 | 'icmpv4Code': None, 3 | 'icmpv4Type': 'ECHO_REPLY', 4 | 'id': '7d1e807d-545b-11eb-aab5-95f8b07b5659', 5 | 'isSystemDefined': False, 6 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/icmpv4ports/7d1e807d-545b-11eb-aab5-95f8b07b5659'}, 7 | 'name': 'ec reply', 8 | 'type': 'icmpv4portobject', 9 | 'version': 'loxpplznuzrjn'}, 10 | {'description': None, 11 | 'icmpv4Code': 'NET_UNREACHABLE', 12 | 'icmpv4Type': 'DESTINATION_UNREACHABLE', 13 | 'id': 'fb816ea2-545c-11eb-aab5-5dd867fd255a', 14 | 'isSystemDefined': False, 15 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/icmpv4ports/fb816ea2-545c-11eb-aab5-5dd867fd255a'}, 16 | 'name': 'ICMP_UNREACHABLE', 17 | 'type': 'icmpv4portobject', 18 | 'version': 'bf3gltejkuiws'}] -------------------------------------------------------------------------------- /docs/fp_output/get_icmp_ports_params.txt: -------------------------------------------------------------------------------- 1 | af='6' 2 | 3 | [{'description': 'A very large ICMPv6', 4 | 'icmpv6Code': None, 5 | 'icmpv6Type': 'PACKET_TOO_BIG', 6 | 'id': '10d19dae-5458-11eb-aab5-e946ff8eb526', 7 | 'isSystemDefined': False, 8 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/icmpv6ports/10d19dae-5458-11eb-aab5-e946ff8eb526'}, 9 | 'name': 'BIG6', 10 | 'type': 'icmpv6portobject', 11 | 'version': 'lrerbmosmioew'}] -------------------------------------------------------------------------------- /docs/fp_output/get_intrusion_policies.txt: -------------------------------------------------------------------------------- 1 | [{'description': 'Security Over Connectivity Layer', 2 | 'id': '6a75c525-4d44-11eb-9e04-6f0a2ca42b30', 3 | 'inspectionMode': 'PREVENTION', 4 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/policy/intrusionpolicies/6a75c525-4d44-11eb-9e04-6f0a2ca42b30'}, 5 | 'name': 'Security Over Connectivity', 6 | 'rules': {'links': {'self': 'https://192.168.133.7/api/fdm/latest/policy/intrusionpolicies/6a75c525-4d44-11eb-9e04-6f0a2ca42b30/intrusionrules'}}, 7 | 'type': 'intrusionpolicy', 8 | 'version': 'm7z67ffrxabw'}, 9 | {'description': 'Balanced Security and Connectivity Layer', 10 | 'id': '7481ca70-4d44-11eb-9e04-af619bd5bf8e', 11 | 'inspectionMode': 'PREVENTION', 12 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/policy/intrusionpolicies/7481ca70-4d44-11eb-9e04-af619bd5bf8e'}, 13 | 'name': 'Balanced Security and Connectivity', 14 | 'rules': {'links': {'self': 'https://192.168.133.7/api/fdm/latest/policy/intrusionpolicies/7481ca70-4d44-11eb-9e04-af619bd5bf8e/intrusionrules'}}, 15 | 'type': 'intrusionpolicy', 16 | 'version': 'j6fstu4h5qlna'}, 17 | {'description': 'Connectivity Over Security Layer', 18 | 'id': '7bce629b-4d44-11eb-9e04-77616a49e58c', 19 | 'inspectionMode': 'PREVENTION', 20 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/policy/intrusionpolicies/7bce629b-4d44-11eb-9e04-77616a49e58c'}, 21 | 'name': 'Connectivity Over Security', 22 | 'rules': {'links': {'self': 'https://192.168.133.7/api/fdm/latest/policy/intrusionpolicies/7bce629b-4d44-11eb-9e04-77616a49e58c/intrusionrules'}}, 23 | 'type': 'intrusionpolicy', 24 | 'version': 'phs2e2tlvyhdn'}, 25 | {'description': 'Maximum Detection Layer', 26 | 'id': '80875699-4d44-11eb-9e04-190e1a7b8344', 27 | 'inspectionMode': 'PREVENTION', 28 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/policy/intrusionpolicies/80875699-4d44-11eb-9e04-190e1a7b8344'}, 29 | 'name': 'Maximum Detection', 30 | 'rules': {'links': {'self': 'https://192.168.133.7/api/fdm/latest/policy/intrusionpolicies/80875699-4d44-11eb-9e04-190e1a7b8344/intrusionrules'}}, 31 | 'type': 'intrusionpolicy', 32 | 'version': 'nlypnji4gaseu'} -------------------------------------------------------------------------------- /docs/fp_output/get_net_groups.txt: -------------------------------------------------------------------------------- 1 | [{'description': 'GROUP-HOST1', 2 | 'id': '26dbfd53-4db6-11eb-aab5-4dba63992bb3', 3 | 'isSystemDefined': False, 4 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networkgroups/26dbfd53-4db6-11eb-aab5-4dba63992bb3'}, 5 | 'name': 'GROUP-HOST1', 6 | 'objects': [{'id': '9f42dab3-4db5-11eb-aab5-19160f885ae8', 7 | 'name': 'Host1-NIC1', 8 | 'type': 'networkobject', 9 | 'version': 'jpfwstwwapru5'}, 10 | {'id': '9ffc3fa6-4db5-11eb-aab5-23e5ffdb3f0d', 11 | 'name': 'Host1-NIC2', 12 | 'type': 'networkobject', 13 | 'version': 'joq4kyvc4ztx5'}], 14 | 'type': 'networkobjectgroup', 15 | 'version': 'lcplbsj24ebva'}, 16 | {'description': 'GROUP-HOST2', 17 | 'id': '28368446-4db6-11eb-aab5-b7a83f9515f7', 18 | 'isSystemDefined': False, 19 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networkgroups/28368446-4db6-11eb-aab5-b7a83f9515f7'}, 20 | 'name': 'GROUP-HOST2', 21 | 'objects': [{'id': 'a13ab322-4db5-11eb-aab5-5330425fbd55', 22 | 'name': 'Host2-NIC1', 23 | 'type': 'networkobject', 24 | 'version': 'ddha6de2szejr'}, 25 | {'id': 'a183f125-4db5-11eb-aab5-4b89ea1eb596', 26 | 'name': 'Host2-NIC2', 27 | 'type': 'networkobject', 28 | 'version': 'makivftpuepn'}], 29 | 'type': 'networkobjectgroup', 30 | 'version': 'd7fsrmu7qvlna'}, 31 | {'description': 'GROUP-ALL-HOSTS', 32 | 'id': '2904a9b9-4db6-11eb-aab5-93170f9c3b34', 33 | 'isSystemDefined': False, 34 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networkgroups/2904a9b9-4db6-11eb-aab5-93170f9c3b34'}, 35 | 'name': 'GROUP-ALL-HOSTS', 36 | 'objects': [{'id': '26dbfd53-4db6-11eb-aab5-4dba63992bb3', 37 | 'name': 'GROUP-HOST1', 38 | 'type': 'networkobjectgroup', 39 | 'version': 'lcplbsj24ebva'}, 40 | {'id': '28368446-4db6-11eb-aab5-b7a83f9515f7', 41 | 'name': 'GROUP-HOST2', 42 | 'type': 'networkobjectgroup', 43 | 'version': 'd7fsrmu7qvlna'}], 44 | 'type': 'networkobjectgroup', 45 | 'version': 'i4oe7b4lpaxos'}] -------------------------------------------------------------------------------- /docs/fp_output/get_net_groups_params.txt: -------------------------------------------------------------------------------- 1 | 'GROUP-ALL-HOSTS' 2 | 3 | {'description': 'GROUP-ALL-HOSTS', 4 | 'id': '2904a9b9-4db6-11eb-aab5-93170f9c3b34', 5 | 'isSystemDefined': False, 6 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networkgroups/2904a9b9-4db6-11eb-aab5-93170f9c3b34'}, 7 | 'name': 'GROUP-ALL-HOSTS', 8 | 'objects': [{'id': '26dbfd53-4db6-11eb-aab5-4dba63992bb3', 9 | 'name': 'GROUP-HOST1', 10 | 'type': 'networkobjectgroup', 11 | 'version': 'lcplbsj24ebva'}, 12 | {'id': '28368446-4db6-11eb-aab5-b7a83f9515f7', 13 | 'name': 'GROUP-HOST2', 14 | 'type': 'networkobjectgroup', 15 | 'version': 'd7fsrmu7qvlna'}], 16 | 'type': 'networkobjectgroup', 17 | 'version': 'i4oe7b4lpaxos'} -------------------------------------------------------------------------------- /docs/fp_output/get_net_obj_or_grp_params.txt: -------------------------------------------------------------------------------- 1 | 'Host1-NIC1' 2 | 3 | {'description': 'HOST1-NIC1', 4 | 'dnsResolution': 'IPV4_ONLY', 5 | 'id': '9f42dab3-4db5-11eb-aab5-19160f885ae8', 6 | 'isSystemDefined': False, 7 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networks/9f42dab3-4db5-11eb-aab5-19160f885ae8'}, 8 | 'name': 'Host1-NIC1', 9 | 'subType': 'HOST', 10 | 'type': 'networkobject', 11 | 'value': '10.0.1.1', 12 | 'version': 'jpfwstwwapru5'} -------------------------------------------------------------------------------- /docs/fp_output/get_net_objects.txt: -------------------------------------------------------------------------------- 1 | [{'description': None, 2 | 'dnsResolution': None, 3 | 'id': '00f7b297-4d44-11eb-9e04-13721b05d633', 4 | 'isSystemDefined': True, 5 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networks/00f7b297-4d44-11eb-9e04-13721b05d633'}, 6 | 'name': 'any-ipv4', 7 | 'subType': 'NETWORK', 8 | 'type': 'networkobject', 9 | 'value': '0.0.0.0/0', 10 | 'version': 'kxd2dzxm2gtwn'}, 11 | {'description': None, 12 | 'dnsResolution': None, 13 | 'id': '0107df38-4d44-11eb-9e04-5b0093cb3558', 14 | 'isSystemDefined': True, 15 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networks/0107df38-4d44-11eb-9e04-5b0093cb3558'}, 16 | 'name': 'any-ipv6', 17 | 'subType': 'NETWORK', 18 | 'type': 'networkobject', 19 | 'value': '::/0', 20 | 'version': 'ezvnwzxqrq6pj'}, 21 | {'description': 'HOST1-NIC1', 22 | 'dnsResolution': 'IPV4_ONLY', 23 | 'id': '9f42dab3-4db5-11eb-aab5-19160f885ae8', 24 | 'isSystemDefined': False, 25 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networks/9f42dab3-4db5-11eb-aab5-19160f885ae8'}, 26 | 'name': 'Host1-NIC1', 27 | 'subType': 'HOST', 28 | 'type': 'networkobject', 29 | 'value': '10.0.1.1', 30 | 'version': 'jpfwstwwapru5'}, 31 | ] -------------------------------------------------------------------------------- /docs/fp_output/get_net_objects_params.txt: -------------------------------------------------------------------------------- 1 | 'Host1-NIC1' 2 | 3 | {'description': 'HOST1-NIC1', 4 | 'dnsResolution': 'IPV4_ONLY', 5 | 'id': '9f42dab3-4db5-11eb-aab5-19160f885ae8', 6 | 'isSystemDefined': False, 7 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/networks/9f42dab3-4db5-11eb-aab5-19160f885ae8'}, 8 | 'name': 'Host1-NIC1', 9 | 'subType': 'HOST', 10 | 'type': 'networkobject', 11 | 'value': '10.0.1.1', 12 | 'version': 'jpfwstwwapru5'} -------------------------------------------------------------------------------- /docs/fp_output/get_ospf_settings.txt: -------------------------------------------------------------------------------- 1 | [{'areas': [{'areaId': '0', 2 | 'areaNetworks': [{'ipv4Network': {'id': '00f7b297-4d44-11eb-9e04-13721b05d633', 3 | 'name': 'any-ipv4', 4 | 'type': 'networkobject', 5 | 'version': 'kxd2dzxm2gtwn'}, 6 | 'tagInterface': None, 7 | 'type': 'areanetwork'}], 8 | 'areaRanges': [], 9 | 'areaType': None, 10 | 'authentication': None, 11 | 'defaultCost': None, 12 | 'filterList': [], 13 | 'type': 'area', 14 | 'virtualLinks': []}], 15 | 'description': '', 16 | 'filterRules': [], 17 | 'id': '2edf8a2a-7948-11eb-b948-7146721ce7c1', 18 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/devices/default/routing/virtualrouters/42e95fbf-fd5a-42bf-a95f-bffd5a42bfd6/ospf/2edf8a2a-7948-11eb-b948-7146721ce7c1'}, 19 | 'logAdjacencyChanges': {'logType': 'DETAILED', 'type': 'logadjacencychanges'}, 20 | 'name': 'OSPF-Settings', 21 | 'neighbors': [], 22 | 'processConfiguration': {'administrativeDistance': {'external': 110, 23 | 'interArea': 110, 24 | 'intraArea': 110, 25 | 'type': 'administrativedistance'}, 26 | 'defaultInformationOriginate': None, 27 | 'ignoreLsaMospf': False, 28 | 'nsfGracefulRestart': None, 29 | 'rfc1583Compatible': None, 30 | 'routerId': '192.168.133.7', 31 | 'timers': {'floodPacing': 33, 32 | 'lsaArrival': 1000, 33 | 'lsaGroup': 240, 34 | 'lsaThrottleTimer': {'initialDelay': 0, 35 | 'maximumDelay': 5000, 36 | 'minimumDelay': 5000, 37 | 'type': 'lsathrottletimer'}, 38 | 'retransmission': 66, 39 | 'spfThrottleTimer': {'initialDelay': 5000, 40 | 'maximumWaitTime': 10000, 41 | 'minimumHoldTime': 10000, 42 | 'type': 'spfthrottletimer'}, 43 | 'type': 'timers'}, 44 | 'type': 'processconfiguration'}, 45 | 'processId': '1', 46 | 'redistributeProtocols': [], 47 | 'summaryAddresses': [], 48 | 'type': 'ospf', 49 | 'version': 'bkgiho4cwbm4l'}] -------------------------------------------------------------------------------- /docs/fp_output/get_pending_changes.txt: -------------------------------------------------------------------------------- 1 | [{'entityId': '3a89b463-4d44-11eb-9e04-bf71277f48e6', 2 | 'entityName': None, 3 | 'entityType': 'devicehostname', 4 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/operational/pendingchanges/'}, 5 | 'referencesAdded': [], 6 | 'referencesDeleted': [], 7 | 'type': 'entityupdate', 8 | 'valuesAdded': [], 9 | 'valuesDeleted': [], 10 | 'valuesUpdated': [{'fieldName': 'hostname', 11 | 'newValue': 'firepyer2120', 12 | 'oldValue': 'firepower', 13 | 'type': 'valueupdate'}]}, 14 | {'entityId': '5c405b75-5105-11eb-aab5-0140ccd4feb7', 15 | 'entityName': 'very-secure-rule', 16 | 'entityType': 'accessrule', 17 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/operational/pendingchanges/'}, 18 | 'referencesAdded': [], 19 | 'type': 'entitycreate', 20 | 'valuesAdded': [{'fieldName': 'logFiles', 21 | 'newValue': 'false', 22 | 'type': 'valueadd'}, 23 | {'fieldName': 'eventLogAction', 24 | 'newValue': 'LOG_NONE', 25 | 'type': 'valueadd'}, 26 | {'fieldName': 'ruleId', 27 | 'newValue': '268435505', 28 | 'type': 'valueadd'}, 29 | {'fieldName': 'name', 30 | 'newValue': 'very-secure-rule', 31 | 'type': 'valueadd'}]}, 32 | {'entityId': 'c78e66bc-cb57-43fe-bcbf-96b79b3475b3', 33 | 'entityName': 'NGFW-Access-Policy', 34 | 'entityType': 'accesspolicy', 35 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/operational/pendingchanges/'}, 36 | 'referencesAdded': [], 37 | 'referencesDeleted': [], 38 | 'type': 'entityupdate', 39 | 'valuesAdded': [], 40 | 'valuesDeleted': [], 41 | 'valuesUpdated': []}] -------------------------------------------------------------------------------- /docs/fp_output/get_port_groups.txt: -------------------------------------------------------------------------------- 1 | [{'description': None, 2 | 'id': '9722eb6c-503c-11eb-aab5-e5660269d013', 3 | 'isSystemDefined': False, 4 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/portgroups/9722eb6c-503c-11eb-aab5-e5660269d013'}, 5 | 'name': 'MY-APP-GROUP', 6 | 'objects': [{'id': '96128cd6-503c-11eb-aab5-3106163baeae', 7 | 'name': 'MY-APP8080', 8 | 'type': 'tcpportobject', 9 | 'version': 'o6uofdgc7jjqg'}], 10 | 'type': 'portobjectgroup', 11 | 'version': 'najpv3ahvumwg'}, 12 | {'description': None, 13 | 'id': '96b24f49-503c-11eb-aab5-538789755de0', 14 | 'isSystemDefined': False, 15 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/portgroups/96b24f49-503c-11eb-aab5-538789755de0'}, 16 | 'name': 'MY-HTTP-GROUP', 17 | 'objects': [{'id': '959aebd0-503c-11eb-aab5-5962006643ab', 18 | 'name': 'MY-HTTP8001', 19 | 'type': 'tcpportobject', 20 | 'version': 'j52i2zhi2vdmj'}, 21 | {'id': '95cc3503-503c-11eb-aab5-99f174e74471', 22 | 'name': 'MY-HTTP8002', 23 | 'type': 'tcpportobject', 24 | 'version': 'og4ujvn4b7gvt'}], 25 | 'type': 'portobjectgroup', 26 | 'version': 'lb2muyfaazmcz'}] -------------------------------------------------------------------------------- /docs/fp_output/get_port_obj_or_grp_params.txt: -------------------------------------------------------------------------------- 1 | 'BIG6' 2 | 3 | {'description': 'A very large ICMPv6', 4 | 'icmpv6Code': None, 5 | 'icmpv6Type': 'PACKET_TOO_BIG', 6 | 'id': '10d19dae-5458-11eb-aab5-e946ff8eb526', 7 | 'isSystemDefined': False, 8 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/icmpv6ports/10d19dae-5458-11eb-aab5-e946ff8eb526'}, 9 | 'name': 'BIG6', 10 | 'type': 'icmpv6portobject', 11 | 'version': 'lrerbmosmioew'} -------------------------------------------------------------------------------- /docs/fp_output/get_syslog_servers.txt: -------------------------------------------------------------------------------- 1 | [{'deviceInterface': {'hardwareName': 'GigabitEthernet0/1', 2 | 'id': 'ad6a9497-4d44-11eb-9e04-63d0b1958967', 3 | 'name': 'inside', 4 | 'type': 'physicalinterface', 5 | 'version': 'eqotynhtlcuyf'}, 6 | 'host': '192.168.0.53', 7 | 'id': '00f2c4e0-52cf-11eb-aab5-55a503dce30e', 8 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/object/syslogalerts/00f2c4e0-52cf-11eb-aab5-55a503dce30e'}, 9 | 'name': '192.168.0.53:514', 10 | 'port': '514', 11 | 'protocol': 'UDP', 12 | 'type': 'syslogserver', 13 | 'useManagementInterface': False, 14 | 'version': 'gz7i7ht2njk6r'}] -------------------------------------------------------------------------------- /docs/fp_output/get_system_info.txt: -------------------------------------------------------------------------------- 1 | {'applianceUuid': '12345678-1234-1234-1234-123456789abc', 2 | 'currentTime': 1610202358116, 3 | 'databaseInfo': {'buildVersion': '6.6.1', 4 | 'configDBVersion': 'itrncf35kvb2q', 5 | 'firewallConfigChecksum': 'kptuzmstdl7ve', 6 | 'id': '00000001-0000-0000-0000-000000000001', 7 | 'isBootstrapSuccessFul': True, 8 | 'restoredFromBackup': 'NONE', 9 | 'schemaVersion': '91', 10 | 'softwareVersion': '6.6.1-91', 11 | 'type': 'databaseinfo'}, 12 | 'geolocationVersion': {'geolocationDbVersion': None, 13 | 'id': 'e2822936-b3ff-11e5-b2b0-75809939b187', 14 | 'lastSuccessGeolocationDate': None, 15 | 'name': None, 16 | 'type': 'geolocationversion'}, 17 | 'id': 'default', 18 | 'ipv4': '192.168.133.7', 19 | 'ipv6': None, 20 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/operational/systeminfo/default'}, 21 | 'managementInterfaceName': 'br1', 22 | 'modelId': 'B', 23 | 'modelNumber': '75', 24 | 'platformModel': 'Cisco Firepower Threat Defense for KVM', 25 | 'securityIntelligenceFeedsInfo': {'id': 'f78b4875-4d43-11eb-9e04-838d9f38aad2', 26 | 'lastFeedsUpdateDate': None, 27 | 'type': 'securityintelligencefeedsinfo'}, 28 | 'serialNumber': '123ABC123AB', 29 | 'snortVersion': {'id': '00000003-0000-0000-0000-000000000003', 30 | 'name': '2.9.16-1025', 31 | 'snortPackage': '/ngfw/var/sf/snort-2.9.16-1025/snort-75-2.9.16-1025-daq11.tar.bz2', 32 | 'snortVersion': '2.9.16-1025', 33 | 'type': 'snortversion'}, 34 | 'softwareVersion': '6.6.1-91', 35 | 'sruVersion': {'id': '00000003-0000-0000-0000-000000000001', 36 | 'lastSuccessSRUDate': '2021-01-02 23:12:33Z', 37 | 'name': '2020-08-18-001-vrt', 38 | 'soMd5Checksum': 'f49e3ed1bfe85316b8b050ebfa99e52b', 39 | 'sruVersion': '2020-08-18-001-vrt', 40 | 'type': 'sruversion'}, 41 | 'systemUptime': 1609628582697, 42 | 'type': 'systeminformation', 43 | 'vdbVersion': {'appIDRevision': '74', 44 | 'id': '00000003-0000-0000-0000-000000000002', 45 | 'lastSuccessVDBDate': None, 46 | 'name': '336', 47 | 'navlRevision': '98', 48 | 'type': 'vdbversion', 49 | 'vdbCurrentBuild': '0', 50 | 'vdbCurrentVersion': '336', 51 | 'vdbReleaseDate': '2020-06-15 16:38:24'}} -------------------------------------------------------------------------------- /docs/fp_output/get_upgrade_files.txt: -------------------------------------------------------------------------------- 1 | [{'fileSize': 1288104, 2 | 'id': 'dff28977-4d48-11eb-ad95-5dbe3d6f985f', 3 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/managedentity/upgradefiles/dff28977-4d48-11eb-ad95-5dbe3d6f985f'}, 4 | 'rebootRequired': True, 5 | 'type': 'upgradefile', 6 | 'updateVersion': '6.6.1-91', 7 | 'upgradeCancelOnFailureDefault': None, 8 | 'upgradeFileName': 'Cisco_FTD_Upgrade-6.6.1-91.sh.REL.tar', 9 | 'upgradeFrom': '6.2.3', 10 | 'upgradeType': 'Cisco FTD Upgrade', 11 | 'uploadDate': 'Sat Jan 02 22:21:35 UTC 2021', 12 | 'user': 'admin', 13 | 'version': 'eujn2nhpfebcl'}] -------------------------------------------------------------------------------- /docs/fp_output/get_vrfs.txt: -------------------------------------------------------------------------------- 1 | [{'description': "Customer A's VRF", 2 | 'id': '67e4d858-503d-11eb-aab5-2921a41f8ca3', 3 | 'interfaces': [{'hardwareName': 'GigabitEthernet0/2', 4 | 'id': 'aeb5b238-4d44-11eb-9e04-cd44159d2943', 5 | 'name': 'customer_a', 6 | 'type': 'physicalinterface', 7 | 'version': 'nh7piq3rw7pzs'}], 8 | 'isSystemDefined': False, 9 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/devices/default/routing/virtualrouters/67e4d858-503d-11eb-aab5-2921a41f8ca3'}, 10 | 'name': 'Customer-A', 11 | 'type': 'virtualrouter', 12 | 'version': 'crdwtc44cg5pu'}, 13 | {'description': "Customer B's VRF", 14 | 'id': '7360254c-503d-11eb-aab5-41ec0935f001', 15 | 'interfaces': [{'hardwareName': 'GigabitEthernet0/3', 16 | 'id': 'afb288c9-4d44-11eb-9e04-41c0f86d8474', 17 | 'name': 'customer_b', 18 | 'type': 'physicalinterface', 19 | 'version': 'ocdhtp76zpfzz'}], 20 | 'isSystemDefined': False, 21 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/devices/default/routing/virtualrouters/7360254c-503d-11eb-aab5-41ec0935f001'}, 22 | 'name': 'Customer-B', 23 | 'type': 'virtualrouter', 24 | 'version': 'nl7onsmfqdujm'}, 25 | {'description': 'This is a Global Virtual Router', 26 | 'id': '42e95fbf-fd5a-42bf-a95f-bffd5a42bfd6', 27 | 'interfaces': [{'hardwareName': 'Management0/0', 28 | 'id': 'b0b5a0ea-4d44-11eb-9e04-43089048338b', 29 | 'name': 'diagnostic', 30 | 'type': 'physicalinterface', 31 | 'version': 'inmqiea7woymm'}, 32 | {'hardwareName': 'GigabitEthernet0/1', 33 | 'id': 'ad6a9497-4d44-11eb-9e04-63d0b1958967', 34 | 'name': 'inside', 35 | 'type': 'physicalinterface', 36 | 'version': 'eqotynhtlcuyf'}, 37 | {'hardwareName': 'GigabitEthernet0/0', 38 | 'id': '8d6c41df-3e5f-465b-8e5a-d336b282f93f', 39 | 'name': 'outside', 40 | 'type': 'physicalinterface', 41 | 'version': 'h4kqp4iu2yvff'}], 42 | 'isSystemDefined': True, 43 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/devices/default/routing/virtualrouters/42e95fbf-fd5a-42bf-a95f-bffd5a42bfd6'}, 44 | 'name': 'Global', 45 | 'type': 'virtualrouter', 46 | 'version': 'cna3vbajed6et'}] -------------------------------------------------------------------------------- /docs/fp_output/read_groups_csv_params.txt: -------------------------------------------------------------------------------- 1 | 'network_groups.csv' 2 | 3 | [{'description': 'Host1', 4 | 'name': 'GROUP-HOST1', 5 | 'objects': ['Host1-NIC1', 6 | 'Host1-NIC2']}, 7 | {'description': 'Host2', 8 | 'name': 'GROUP-HOST2', 9 | 'objects': ['Host2-NIC1', 10 | 'Host2-NIC2']}, 11 | {'description': 'All hosts', 12 | 'name': 'GROUP-ALL-HOSTS', 13 | 'objects': ['GROUP-HOST1', 'GROUP-HOST2']}] -------------------------------------------------------------------------------- /docs/fp_output/read_objects_csv_params.txt: -------------------------------------------------------------------------------- 1 | 'network_objects.csv' 2 | 3 | [{'description': 'HOST1-NIC1', 4 | 'name': 'Host1-NIC1', 5 | 'type': 'host', 6 | 'value': '10.0.1.1'}, 7 | {'description': 'HOST1-NIC2', 8 | 'name': 'Host1-NIC2', 9 | 'type': 'host', 10 | 'value': '10.0.1.2'}] -------------------------------------------------------------------------------- /docs/fp_output/send_command_params.txt: -------------------------------------------------------------------------------- 1 | 'show interface ip brief' 2 | 3 | Interface IP-Address OK? Method Status Protocol 4 | GigabitEthernet0/0 unassigned YES DHCP up up 5 | GigabitEthernet0/1 192.168.45.1 YES manual up up 6 | GigabitEthernet0/2 unassigned YES unset administratively down up 7 | GigabitEthernet0/3 unassigned YES unset administratively down up 8 | Internal-Control0/0 127.0.1.1 YES unset up up 9 | Internal-Control0/1 unassigned YES unset up up 10 | Internal-Data0/0 unassigned YES unset down up 11 | Internal-Data0/0 unassigned YES unset up up 12 | Internal-Data0/1 169.254.1.1 YES unset up up 13 | Internal-Data0/2 unassigned YES unset up up 14 | Management0/0 unassigned YES unset up up -------------------------------------------------------------------------------- /docs/fp_output/set_bgp_general_settings_params.txt: -------------------------------------------------------------------------------- 1 | asn='65500' 2 | 3 | {'aggregateTimer': 30, 4 | 'asNumber': '65500', 5 | 'asnotationDot': False, 6 | 'bgpBestPath': {'alwaysCompareMed': False, 7 | 'bestPathCompareRouterId': False, 8 | 'bestPathMedMissingAsWorst': False, 9 | 'defaultLocalPreferenceValue': 100, 10 | 'deterministicMed': False, 11 | 'type': 'bgpbestpath'}, 12 | 'bgpGracefulRestart': None, 13 | 'bgpNextHopTriggerDelay': 5, 14 | 'bgpNextHopTriggerEnable': True, 15 | 'bgpTimers': {'holdTime': 180, 16 | 'keepAlive': 60, 17 | 'minHoldTime': 0, 18 | 'type': 'bgptimers'}, 19 | 'description': None, 20 | 'enforceFirstAs': True, 21 | 'fastExternalFallOver': True, 22 | 'id': '12345678-1234-1234-1234-123456789abc', 23 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/devices/default/routing/bgpgeneralsettings/12345678-1234-1234-1234-123456789abc'}, 24 | 'logNeighborChanges': True, 25 | 'maxasLimit': None, 26 | 'name': 'BgpGeneralSettings', 27 | 'routerId': None, 28 | 'scanTime': 60, 29 | 'transportPathMtuDiscovery': True, 30 | 'type': 'bgpgeneralsettings', 31 | 'version': 'omg123ffsabcd'} -------------------------------------------------------------------------------- /docs/fp_output/update_intrusion_rules.txt: -------------------------------------------------------------------------------- 1 | {'deployAfterUpdate': False, 2 | 'description': None, 3 | 'forceOperation': False, 4 | 'forceUpdate': False, 5 | 'id': 'ddbc819d-840a-11eb-ab2b-479260419be4', 6 | 'ipAddress': '192.168.133.100', 7 | 'jobHistoryUuid': 'ddccfc5e-840a-11eb-ab2b-4dced09e21e1', 8 | 'jobName': 'Rule Update', 9 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/action/updatesru/ddbc819d-840a-11eb-ab2b-479260419be4'}, 10 | 'name': None, 11 | 'scheduleType': 'IMMEDIATE', 12 | 'sruImmediateJobType': 'SRU_UPDATE', 13 | 'type': 'sruupdateimmediate', 14 | 'user': 'admin', 15 | 'version': 'ivefnacxlum6b'} -------------------------------------------------------------------------------- /docs/fp_output/upload_config_params.txt: -------------------------------------------------------------------------------- 1 | 'full_config-1.txt' 2 | 3 | {'dateModified': '2021-03-13 16:24:10Z', 4 | 'diskFileName': 'full_config-1.txt', 5 | 'id': 'default', 6 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/action/uploadconfigfile/default'}, 7 | 'sizeBytes': 69658, 8 | 'type': 'configimportexportfileinfo'} -------------------------------------------------------------------------------- /docs/fp_output/upload_vdb_file_params.txt: -------------------------------------------------------------------------------- 1 | 'uploads/Cisco_VDB_Fingerprint_Database-4.5.0-337.sh.REL.tar' 2 | 3 | {'checkSum': '231e053a4d9de54e7c03a6e64338c5039150a69c693907c6361a9f115f5e78a4f7f0f05ba98d1bd6f835f0d0b0504b582c70b0485b308b6095841e5de157b1bb', 4 | 'fileName': 'Cisco_VDB_Fingerprint_Database-4.5.0-337.sh.REL.tar', 5 | 'id': None, 6 | 'links': {'self': 'https://192.168.133.7/api/fdm/latest/action/updatevdbfromfile/null'}, 7 | 'name': 'Cisco_VDB_Fingerprint_Database-4.5.0-337.sh.REL.tar', 8 | 'type': 'vdbfileupload', 9 | 'version': None} -------------------------------------------------------------------------------- /docs/fpoutput.py: -------------------------------------------------------------------------------- 1 | from docutils.parsers.rst import Directive 2 | from docutils import nodes 3 | from sphinx.util.nodes import set_source_info 4 | import os 5 | import re 6 | 7 | 8 | def setup(app): 9 | app.add_directive('fp_output', OutputDirective) 10 | 11 | 12 | class OutputDirective(Directive): 13 | required_arguments = 1 14 | optional_arguments = 1 15 | 16 | def run(self): 17 | method = self.arguments[0] 18 | try: 19 | obj_name = self.arguments[1] 20 | except IndexError: 21 | obj_name = 'fdm' 22 | 23 | suffix = '.txt' 24 | assert re.match('^[a-zA-Z][a-zA-Z0-9_]*$', method) 25 | srcdir = self.state.document.settings.env.srcdir 26 | with open(os.path.join(srcdir, 'fp_output', method + suffix)) as fd: 27 | content = fd.read() 28 | if '\n\n' in content: 29 | method = method.split('_params')[0] 30 | params, result = content.split('\n\n') 31 | params = ', '.join(params.split('\n')) 32 | else: 33 | params, result = '', content 34 | 35 | out = f">>> {obj_name}.{method}({params})\n{result}" 36 | literal = nodes.literal_block(out, out) 37 | literal['language'] = 'python' 38 | set_source_info(self, literal) 39 | self.state.parent.children[-1].children[-1].append(literal) 40 | return [] 41 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Firepyer's documentation! 2 | ==================================== 3 | 4 | Firepyer provides a way of interacting with Cisco Firepower devices via their REST APIs in Python. Currently FTD devices using FDM (not FMC) are supported. 5 | The intended usage is to replace some of the tedious clicking tasks from the GUI or perform bulk imports of objects, rules etc. 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | :caption: Getting Started: 10 | 11 | install 12 | using 13 | 14 | 15 | .. toctree:: 16 | :maxdepth: 1 17 | :caption: Features: 18 | 19 | deployments 20 | Interfaces & Zones 21 | networks 22 | ports 23 | routing 24 | policies 25 | System 26 | 27 | 28 | .. toctree:: 29 | :maxdepth: 1 30 | :caption: Misc: 31 | :hidden: 32 | 33 | utils 34 | change 35 | 36 | 37 | * :ref:`genindex` 38 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Installing Firepyer 2 | =================== 3 | 4 | The latest release is available to download from PyPI, simply using ``pip install firepyer``. 5 | 6 | Alternatively, as this project is still in early development, the best place to get the most recent features is directly from the `source GitHub repo `_. -------------------------------------------------------------------------------- /docs/interfaces.rst: -------------------------------------------------------------------------------- 1 | Interface & Security Zones 2 | ========================== 3 | 4 | Interact with physical/logical interfaces and security zones. 5 | 6 | EtherChannels 7 | ------------- 8 | 9 | .. py:currentmodule:: firepyer 10 | 11 | .. class:: Fdm 12 | :noindex: 13 | 14 | Interfaces 15 | ---------- 16 | 17 | .. py:currentmodule:: firepyer 18 | 19 | .. class:: Fdm 20 | :noindex: 21 | 22 | .. automethod:: get_interfaces 23 | 24 | Security Zones 25 | -------------- 26 | 27 | .. py:currentmodule:: firepyer 28 | 29 | .. class:: Fdm 30 | :noindex: 31 | 32 | .. automethod:: create_security_zone 33 | .. automethod:: get_security_zones 34 | 35 | 36 | Sub-Interfaces 37 | -------------- 38 | 39 | .. py:currentmodule:: firepyer 40 | 41 | .. class:: Fdm 42 | :noindex: 43 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/methods: -------------------------------------------------------------------------------- 1 | get_net_groups 2 | get_net_objects 3 | create_access_rule 4 | create_network_group -------------------------------------------------------------------------------- /docs/networks.rst: -------------------------------------------------------------------------------- 1 | Networks 2 | ============= 3 | 4 | Network Objects and Network Groups are used to store IP networks or hosts 5 | as variables, that can be used in Access Policy rules etc. 6 | 7 | 8 | .. py:currentmodule:: firepyer 9 | 10 | .. class:: Fdm 11 | :noindex: 12 | 13 | .. automethod:: create_net_group 14 | .. fp_output:: create_net_group_params 15 | .. automethod:: create_network 16 | .. fp_output:: create_network_params 17 | .. automethod:: delete_network 18 | .. automethod:: delete_network_group 19 | .. automethod:: get_net_groups 20 | .. fp_output:: get_net_groups 21 | .. fp_output:: get_net_groups_params 22 | .. automethod:: get_net_objects 23 | .. fp_output:: get_net_objects 24 | .. fp_output:: get_net_objects_params 25 | .. automethod:: get_net_obj_or_grp 26 | .. fp_output:: get_net_obj_or_grp_params 27 | -------------------------------------------------------------------------------- /docs/policies.rst: -------------------------------------------------------------------------------- 1 | Security Policies 2 | ================= 3 | 4 | .. py:currentmodule:: firepyer 5 | 6 | .. class:: Fdm 7 | :noindex: 8 | 9 | .. automethod:: create_access_rule 10 | .. fp_output:: create_access_rule_params 11 | .. automethod:: delete_access_rule 12 | .. automethod:: get_access_rules 13 | .. fp_output:: get_access_rules 14 | .. automethod:: get_intrusion_policies 15 | .. fp_output:: get_intrusion_policies -------------------------------------------------------------------------------- /docs/ports.rst: -------------------------------------------------------------------------------- 1 | Ports 2 | ============= 3 | 4 | PortObjects and PortGroups are used to store TCP or UDP ports 5 | as variables, that can be used in Access Policy rules etc. 6 | 7 | 8 | .. py:currentmodule:: firepyer 9 | 10 | .. class:: Fdm 11 | :noindex: 12 | 13 | .. automethod:: create_icmp_port 14 | .. fp_output:: create_icmp_port_params 15 | .. automethod:: create_port_group 16 | .. fp_output:: create_port_group_params 17 | .. automethod:: create_port_object 18 | .. fp_output:: create_port_object_params 19 | .. automethod:: get_icmp_ports 20 | .. fp_output:: get_icmp_ports 21 | .. fp_output:: get_icmp_ports_params 22 | .. automethod:: get_port_groups 23 | .. fp_output:: get_port_groups 24 | .. automethod:: get_port_obj_or_grp 25 | .. fp_output:: get_port_obj_or_grp_params 26 | .. automethod:: get_tcp_ports 27 | .. automethod:: get_udp_ports 28 | -------------------------------------------------------------------------------- /docs/routing.rst: -------------------------------------------------------------------------------- 1 | Routing 2 | ============= 3 | 4 | View and configure routing for the FTD device, including VRFs, static routes, OSPF and BGP. 5 | 6 | BGP 7 | --------- 8 | 9 | .. py:currentmodule:: firepyer 10 | 11 | .. class:: Fdm 12 | :noindex: 13 | 14 | .. automethod:: get_bgp_general_settings 15 | .. fp_output:: get_bgp_general_settings 16 | .. automethod:: set_bgp_general_settings 17 | .. fp_output:: set_bgp_general_settings_params 18 | .. automethod:: get_bgp_settings 19 | .. fp_output:: get_bgp_settings 20 | .. automethod:: set_bgp_settings 21 | 22 | 23 | OSPF 24 | --------- 25 | 26 | .. py:currentmodule:: firepyer 27 | 28 | .. class:: Fdm 29 | :noindex: 30 | 31 | .. automethod:: get_ospf_settings 32 | .. fp_output:: get_ospf_settings 33 | 34 | 35 | VRFs 36 | --------- 37 | 38 | .. py:currentmodule:: firepyer 39 | 40 | .. class:: Fdm 41 | :noindex: 42 | 43 | .. automethod:: get_vrfs 44 | .. fp_output:: get_vrfs 45 | -------------------------------------------------------------------------------- /docs/system.rst: -------------------------------------------------------------------------------- 1 | System Settings & Tasks 2 | ======================= 3 | 4 | General system settings. 5 | 6 | Config Import/Export 7 | -------------------- 8 | 9 | .. py:currentmodule:: firepyer 10 | 11 | .. class:: Fdm 12 | :noindex: 13 | 14 | .. automethod:: apply_config_import 15 | .. automethod:: delete_config_file 16 | .. automethod:: download_config_file 17 | .. automethod:: export_config 18 | .. fp_output:: export_config_params 19 | .. automethod:: get_config_files 20 | .. fp_output:: get_config_files 21 | .. automethod:: upload_config 22 | .. fp_output:: upload_config_params 23 | 24 | 25 | General 26 | ------- 27 | 28 | .. py:currentmodule:: firepyer 29 | 30 | .. class:: Fdm 31 | :noindex: 32 | 33 | .. automethod:: create_syslog_server 34 | .. fp_output:: create_syslog_server_params 35 | .. automethod:: get_dhcp_servers 36 | .. fp_output:: get_dhcp_servers 37 | .. automethod:: get_hostname 38 | .. automethod:: get_syslog_servers 39 | .. fp_output:: get_syslog_servers 40 | .. automethod:: get_system_info 41 | .. fp_output:: get_system_info 42 | .. automethod:: send_command 43 | .. fp_output:: send_command_params 44 | .. automethod:: set_hostname 45 | 46 | 47 | Updates 48 | ------- 49 | Methods for updating various rule files 50 | 51 | .. py:currentmodule:: firepyer 52 | 53 | .. class:: Fdm 54 | :noindex: 55 | 56 | .. automethod:: update_intrusion_rules 57 | .. fp_output:: update_intrusion_rules 58 | .. automethod:: update_vdb 59 | .. automethod:: update_geolocation 60 | .. automethod:: upload_intrusion_rule_file 61 | .. automethod:: upload_geolocation_file 62 | .. automethod:: upload_vdb_file 63 | .. fp_output:: upload_vdb_file_params 64 | 65 | 66 | 67 | Upgrades 68 | -------- 69 | Methods for performing system upgrades 70 | 71 | .. py:currentmodule:: firepyer 72 | 73 | .. class:: Fdm 74 | :noindex: 75 | 76 | .. automethod:: get_upgrade_files 77 | .. fp_output:: get_upgrade_files 78 | .. automethod:: upload_upgrade 79 | -------------------------------------------------------------------------------- /docs/using.rst: -------------------------------------------------------------------------------- 1 | Using Firepyer 2 | ================== 3 | 4 | Interacting 5 | ----------- 6 | 7 | All functionailty for interacting with an FTD device is contained within the Fdm class and it's methods. 8 | Authentication is taken care of transparently when calling a method, so this doesn't need to be done explicitly. 9 | 10 | .. module:: firepyer 11 | 12 | .. autoclass:: Fdm 13 | 14 | Import the Fdm class and instantiate an object, passing in your FTD hostname/IP, username and password (and ignoring SSL verification if using an untrusted/self-signed cert): 15 | 16 | >>> from firepyer import Fdm 17 | >>> fdm = Fdm(host='192.168.45.45', username='admin', password='Admin123', verify=False) 18 | 19 | Then call any of the available methods to run against your FTD: 20 | 21 | >>> fdm.get_hostname() 22 | 'firepyer2120' 23 | 24 | >>> fdm.get_net_objects('any-ipv4') 25 | {'description': None, 26 | 'dnsResolution': None, 27 | 'id': '00f7b297-4d44-11eb-9e04-13721b05d633', 28 | 'isSystemDefined': True, 29 | 'links': {'self': 'https://192.168.45.45/api/fdm/latest/object/networks/00f7b297-4d44-11eb-9e04-13721b05d633'}, 30 | 'name': 'any-ipv4', 31 | 'subType': 'NETWORK', 32 | 'type': 'networkobject', 33 | 'value': '0.0.0.0/0', 34 | 'version': 'kxd2dzxm2gtwn'} 35 | 36 | Error Handling 37 | -------------- 38 | Some common errors that may be encountered when using Fdm methods: 39 | 40 | .. module:: firepyer.exceptions 41 | 42 | .. autoexception:: FirepyerAuthError 43 | .. autoexception:: FirepyerError 44 | .. autoexception:: FirepyerInvalidOption 45 | .. autoexception:: FirepyerResourceNotFound 46 | .. autoexception:: FirepyerUnreachableError -------------------------------------------------------------------------------- /docs/utils.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | ========= 3 | A few general utilites for working with the API and it's objects, such as importing data from CSV files. 4 | 5 | .. module:: firepyer.utils 6 | 7 | .. autofunction:: read_objects_csv 8 | 9 | .. fp_output:: read_objects_csv_params utils 10 | 11 | .. code-block:: none 12 | :linenos: 13 | :caption: network_objects.csv 14 | 15 | name,value,type,description 16 | Host1-NIC1,10.0.1.1,host,HOST1-NIC1 17 | Host1-NIC2,10.0.1.2,host,HOST1-NIC2 18 | 19 | 20 | .. autofunction:: read_groups_csv 21 | 22 | .. fp_output:: read_groups_csv_params utils 23 | 24 | .. code-block:: none 25 | :linenos: 26 | :caption: network_groups.csv 27 | 28 | name,objects,description 29 | GROUP-HOST1,Host1-NIC1,Host1 30 | GROUP-HOST1,Host1-NIC2,Host1 31 | GROUP-HOST2,Host2-NIC1,Host2 32 | GROUP-HOST2,Host2-NIC2,Host2 33 | GROUP-ALL-HOSTS,GROUP-HOST1,All hosts 34 | GROUP-ALL-HOSTS,GROUP-HOST2,All hosts 35 | 36 | 37 | 38 | 39 | .. autofunction:: expand_merged_csv 40 | 41 | .. fp_output:: expand_merged_csv_params utils 42 | 43 | .. code-block:: none 44 | :linenos: 45 | :caption: raw_network_groups.csv 46 | 47 | name,objects,description 48 | GROUP-HOST1,Host1-NIC1,Host1 49 | ,Host1-NIC2, 50 | GROUP-HOST2,Host2-NIC1,Host2 51 | ,Host2-NIC2, 52 | GROUP-ALL-HOSTS,GROUP-HOST1,All hosts 53 | ,GROUP-HOST2, 54 | -------------------------------------------------------------------------------- /firepyer/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import json 3 | import logging 4 | from time import sleep 5 | from typing import List 6 | 7 | import requests 8 | from requests.models import Response 9 | from requests_toolbelt.multipart import encoder 10 | 11 | from firepyer.exceptions import FirepyerAuthError, FirepyerError, FirepyerInvalidOption, FirepyerResourceNotFound, FirepyerUnreachableError 12 | 13 | 14 | __version__ = '0.0.7' 15 | 16 | 17 | class Fdm: 18 | def __init__(self, host: str, username: str, password: str, verify: bool = True): 19 | """Provides a connection point to an FTD device 20 | 21 | :param host: The IP or hostname of the FTD device 22 | :type host: str 23 | :param username: Username to login to FDM 24 | :type username: str 25 | :param password: Password to login to FDM 26 | :type password: str 27 | :param verify: Verify the SSL certificate presented by the FTD API, defaults to True 28 | :type verify: bool, optional 29 | """ 30 | self.ftd_host = host 31 | self.username = username 32 | self.password = password 33 | self.access_token = None 34 | self.access_token_expiry_time = None 35 | self.verify = verify 36 | if not verify: 37 | requests.packages.urllib3.disable_warnings() 38 | 39 | def api_call(self, uri, method, data=None, get_auth=True, stream=False, extra_headers=None): 40 | # Check for http allows passing in full URL e.g. from pagination next page link 41 | if 'http' not in uri: 42 | uri = f"https://{self.ftd_host}/api/fdm/latest/{uri}" 43 | 44 | headers = {"Accept": "application/json", 'User-Agent': f'firepyer/{__version__}'} 45 | if get_auth: 46 | headers['Authorization'] = f'Bearer {self._check_get_access_token()}' 47 | if not extra_headers: 48 | # When sending files, requests will auto populate CT as multipart/form-data 49 | headers['Content-Type'] = "application/json" 50 | else: 51 | headers = {**headers, **extra_headers} 52 | 53 | try: 54 | response = requests.request(method, uri, 55 | data=data, 56 | headers=headers, 57 | verify=self.verify, 58 | stream=stream) 59 | if response.status_code == 500: 60 | raise FirepyerError('FTD presented a server error') 61 | elif response.status_code == 503: 62 | raise FirepyerError('FTD responded, but service unavailable (503), may still be booting') 63 | return response 64 | except requests.exceptions.SSLError: 65 | raise FirepyerUnreachableError(f'Failed to connect to {self.ftd_host} due to an SSL error - check certificate or disable verification') 66 | except requests.exceptions.ConnectionError: 67 | raise FirepyerUnreachableError(f'Unable to contact the FTD device at {self.ftd_host}') 68 | 69 | def post_api(self, uri, data=None, get_auth=True, extra_headers=None): 70 | return self.api_call(uri, 'POST', data=data, get_auth=get_auth, extra_headers=extra_headers) 71 | 72 | def put_api(self, uri, data=None): 73 | return self.api_call(uri, 'PUT', data=data) 74 | 75 | def get_api(self, uri, data=None, stream=False): 76 | return self.api_call(uri, 'GET', data=data, stream=stream) 77 | 78 | def get_api_items(self, uri, data=None): 79 | try: 80 | return self.get_api(uri, data).json()['items'] 81 | except KeyError: 82 | # No items field 83 | return None 84 | 85 | def get_api_single_item(self, uri, data=None): 86 | try: 87 | return self.get_api_items(uri, data)[0] 88 | except (IndexError, TypeError): 89 | # Items list is empty or None 90 | return None 91 | 92 | def check_api_status(self): 93 | api_alive = False 94 | 95 | while not api_alive: 96 | try: 97 | api_status = self.api_call('#/login', 'GET', get_auth=False) 98 | except FirepyerUnreachableError: 99 | api_status = None 100 | 101 | if api_status is not None: 102 | if api_status.status_code == 401: 103 | logging.info('API Alive!') 104 | api_alive = True 105 | elif api_status.status_code == 503: 106 | logging.warn('FTD alive, API service unavailable...') 107 | else: 108 | logging.warn('Unable to reach FTD') 109 | sleep(10) 110 | 111 | def _get_access_token(self) -> str: 112 | """ 113 | Login to FTD device and obtain an access token. The access token is required so that the user can 114 | connect to the device to send REST API requests. 115 | :return: OAUTH access token 116 | """ 117 | access_token = None 118 | access_token_expiry_time = None 119 | 120 | payload = f'{{"grant_type": "password", "username": "{self.username}", "password": "{self.password}"}}' 121 | resp = self.post_api('fdm/token', payload, get_auth=False) 122 | if resp.status_code == 400: 123 | raise FirepyerAuthError('Failed to authenticate against FTD - check username/password') 124 | else: 125 | access_token = resp.json().get('access_token') 126 | 127 | epoch_now = datetime.timestamp(datetime.now()) 128 | access_token_valid_secs = 1740 # FDM access token lasts 30mins, this is 29mins 129 | access_token_expiry_time = epoch_now + access_token_valid_secs 130 | 131 | logging.info(f"Login successful, access_token obtained, expires at: {datetime.fromtimestamp(access_token_expiry_time)}") 132 | 133 | return access_token, access_token_expiry_time 134 | 135 | def _check_get_access_token(self) -> str: 136 | """ 137 | Checks if a valid (29mins hasn't passed since obtaining) access token exists, if not gets one 138 | :return: str Either a new or the existing valid access token 139 | """ 140 | 141 | get_token = False 142 | epoch_now = datetime.timestamp(datetime.now()) 143 | 144 | if self.access_token is None: 145 | # No token has been generated yet 146 | get_token = True 147 | elif epoch_now > self.access_token_expiry_time: 148 | # Token expired 149 | get_token = True 150 | 151 | if get_token: 152 | self.access_token, self.access_token_expiry_time = self._get_access_token() 153 | return self.access_token 154 | 155 | def _get_object_subset(self, obj: dict) -> dict: 156 | """ 157 | Gets the significant fields from a full object 158 | :param obj: dict An object retrieved from any endpoint 159 | :return: dict A subet of the uniquely identifiable fields from the object 160 | """ 161 | object_subset = {} 162 | object_subset['id'] = obj['id'] 163 | object_subset['type'] = obj['type'] 164 | object_subset['version'] = obj['version'] 165 | object_subset['name'] = obj['name'] 166 | return object_subset 167 | 168 | def get_class_by_name(self, get_class: dict, obj_name: str, name_field_label: str = 'name', must_find: bool = False) -> dict: 169 | """Get the dict for the Class with the given name 170 | 171 | :param get_class: The 'items' in a GET reponse from an FDM Model query 172 | :type get_class: dict 173 | :param obj_name: The name of the object to find 174 | :type obj_name: str 175 | :param name_field_label: The field to use as the 'name' to match on, defaults to 'name' 176 | :type name_field_label: str, optional 177 | :param must_find: Specifies if an exception should be raised if the resource isn't found, defaults to False 178 | :type must_find: bool, optional 179 | :raises FirepyerResourceNotFound: The resource with the given name could not be found 180 | :return: Object with the name if found, None if not 181 | :rtype: dict 182 | """ 183 | 184 | if get_class is not None: 185 | for obj in get_class: 186 | if obj[name_field_label] == obj_name: 187 | return obj 188 | if must_find: 189 | raise FirepyerResourceNotFound(f'Could not find resource "{obj_name}"') 190 | return None 191 | 192 | def get_paged_items(self, uri: str) -> list: 193 | response = self.get_api(uri).json() 194 | all_items = response.get('items') 195 | # TODO catch this error 196 | if all_items is None: 197 | pass 198 | all_items = response['items'] 199 | next_url = response['paging']['next'] 200 | 201 | while next_url: 202 | response = self.get_api(next_url[0]).json() 203 | all_items += response['items'] 204 | next_url = response['paging']['next'] 205 | 206 | return all_items 207 | 208 | def get_obj_by_filter(self, url, filter): 209 | first_param = '?' 210 | if url.endswith('&'): 211 | first_param = '' 212 | return self.get_api_single_item(f'{url}{first_param}filter={filter}') 213 | 214 | def get_obj_by_name(self, url: str, name: str, must_find: bool = False) -> dict: 215 | """Gets an object of the given resource type (URL) by name 216 | 217 | :param url: URL to look for the resource type 218 | :type url: str 219 | :param name: The name of the resource to find 220 | :type name: str 221 | :param must_find: Specifies if an exception should be raised if the resource isn't found, defaults to False 222 | :type must_find: bool, optional 223 | :raises FirepyerResourceNotFound: The resource with the given name could not be found and must_find is True 224 | :return: A dict of the given object if found, None if not 225 | :rtype: dict|None 226 | """ 227 | if not (obj := self.get_obj_by_filter(url, filter=f'name:{name}')) and must_find: 228 | raise FirepyerResourceNotFound(f'Could not find resource "{name}"') 229 | return obj 230 | 231 | def get_net_objects(self, name='', must_find: bool = False): 232 | """Gets all NetworkObjects or a single NetworkObject if a name is provided 233 | 234 | :param name: The name of the NetworkObject to find, defaults to '' 235 | :type name: str, optional 236 | :param must_find: Specifies if an exception should be raised if the resource isn't found, defaults to False 237 | :type must_find: bool, optional 238 | :return: A list of all NetworkObjects if no name is provided, or a dict of the single NetworkObject with the given name 239 | :rtype: list|dict 240 | """ 241 | if name: 242 | return self.get_obj_by_name('object/networks?limit=0&', name, must_find=must_find) 243 | else: 244 | return self.get_api_items('object/networks?limit=0') 245 | 246 | def get_net_groups(self, name=''): 247 | """Gets all NetworkGroups or a single NetworkGroup if a name is provided 248 | 249 | :param name: The name of a NetworkGroup to find, defaults to '' 250 | :type name: str, optional 251 | :return: A list of all NetworkGroups if no name is provided, or a dict of the single NetworkGroup with the given name 252 | :rtype: list|dict 253 | """ 254 | if name: 255 | return self.get_obj_by_name('object/networkgroups?limit=0&', name) 256 | else: 257 | return self.get_api_items('object/networkgroups?limit=0') 258 | 259 | def get_net_obj_or_grp(self, name) -> dict: 260 | """Get a NetworkObject or NetworkGroup by the given name 261 | 262 | :param name: The name of the object/group to retrieve 263 | :type name: str 264 | :return: Single dict describing the object, if a resource with the name is found 265 | :rtype: dict 266 | """ 267 | net = None 268 | name_filter = {'name': name} 269 | net_finders = [self.get_net_objects, 270 | self.get_net_groups] 271 | 272 | for get_net_method in net_finders: 273 | net = get_net_method(**name_filter) 274 | if net: 275 | break 276 | return net 277 | 278 | def create_network(self, name: str, value: str, type: str = 'HOST', description: str = None) -> dict: 279 | """Creates a network Host, FQDN, Network or Range object 280 | 281 | :param name: Name of the object 282 | :type name: str 283 | :param value: Value of the object, depending on type e.g. Host would be an IP address, Network would be a CIDR network etc. 284 | :type value: str 285 | :param type: Type of Network object to create, defaults to 'HOST' 286 | :type type: str, optional 287 | :param description: Description of the object, defaults to None 288 | :type description: str, optional 289 | :raises FirepyerInvalidOption: If the type is not one of "HOST", "FQDN", "NETWORK" or "RANGE" 290 | :return: The Network object that has been created 291 | :rtype: dict 292 | """ 293 | type = type.upper() 294 | if type not in ['HOST', 'FQDN', 'NETWORK', 'RANGE']: 295 | raise FirepyerInvalidOption('"type" should be one of "HOST", "FQDN", "NETWORK" or "RANGE"') 296 | 297 | host_object = {"name": name, 298 | "description": description, 299 | "subType": type, 300 | "value": value, 301 | "dnsResolution": "IPV4_ONLY", 302 | "type": "networkobject" 303 | } 304 | return self._create_instance('object/networks', host_object) 305 | 306 | def delete_network(self, net_id: str) -> bool: 307 | """Delete a NetworkObject 308 | 309 | :param net_id: NetworkObject id 310 | :type net_id: str 311 | :raises FirepyerResourceNotFound: If a NetworkObject with the given id does not exist 312 | :return: True if the object is successfully deleted 313 | :rtype: bool 314 | """ 315 | return self._delete_instance('object/networks', net_id) 316 | 317 | def _create_instance(self, uri: str, instance_def: dict, friendly_error: str = None) -> dict: 318 | """POSTs the JSON of the provided dict to the URI to create an object and return dict of the created object or raise a friendly error 319 | 320 | :param uri: URI endpoint to send the request to 321 | :type uri: str 322 | :param instance_def: Definition of the object instance to create, the model is defined per object in the API 323 | :type instance_def: dict 324 | :param friendly_error: High level name for task being performed if an error occurs, defaults to None 325 | :type friendly_error: str, optional 326 | :return: The object instance that has been created 327 | :rtype: dict 328 | """ 329 | resp = self.post_api(uri=uri, data=json.dumps(instance_def)) 330 | return self._check_post_response(resp=resp, friendly_error=friendly_error) 331 | 332 | def _delete_instance(self, uri: str, object_id: str) -> bool: 333 | resp = self.api_call(uri=f'{uri}/{object_id}', method='DELETE') 334 | 335 | if resp.status_code == 204: 336 | return True 337 | elif resp.status_code == 422 or 400: 338 | try: 339 | errs = [err for err in resp.json()['error']['messages']] 340 | if 'invalidUuid' in [code.get('code') for code in errs]: 341 | raise FirepyerResourceNotFound(f'Could not find object "{object_id}" to delete') 342 | else: 343 | errs = [err.get("description") for err in errs] 344 | except KeyError: 345 | errs = resp.json() 346 | raise FirepyerError(f'Unable to delete object "{object_id}" due to the following error(s): {errs}') 347 | else: 348 | raise FirepyerError(f'This isn\'t supposed to happen: {resp}') 349 | 350 | def _check_post_response(self, resp: Response, friendly_error: str = None) -> dict: 351 | """Checks the reponse of a POST and returns a dict of the created object instance or raises a friendly error 352 | 353 | :param resp: Response from the POST request to create an object 354 | :type resp: Response 355 | :param friendly_error: High level name for task being performed if an error occurs, defaults to None 356 | :type friendly_error: str, optional 357 | :raises FirepyerError: If any server-side errors occur the description(s) will be passed through 358 | :return: The object instance that has been created 359 | :rtype: dict 360 | """ 361 | 362 | if not friendly_error: 363 | friendly_error = 'create resource' 364 | 365 | if resp.status_code == 200: 366 | return resp.json() 367 | elif resp.status_code == 422 or 400: 368 | try: 369 | err_msgs = [err['description'] for err in resp.json()['error']['messages']] 370 | except KeyError: 371 | err_msgs = resp.json() 372 | raise FirepyerError(f'Unable to {friendly_error} due to the following error(s): {err_msgs}') 373 | else: 374 | raise FirepyerError(f'This isn\'t supposed to happen: {resp}') 375 | 376 | def create_group(self, name: str, group_type: str, objects_for_group: List[dict], description: str = None) -> dict: 377 | """Creates a group of pre-existing Network or Port objects 378 | 379 | :param name: Name of the group being created 380 | :type name: str 381 | :param group_type: Should be either 'network' or 'port' depending on group class 382 | :type group_type: str 383 | :param objects_for_group: All API-gathered Objects to be added to the group 384 | :type objects_for_group: List[dict] 385 | :param description: Description of the group being created, defaults to None 386 | :type description: str, optional 387 | :return: The created group object 388 | :rtype: dict 389 | """ 390 | object_group = {"name": name, 391 | "description": description, 392 | "objects": objects_for_group, 393 | "type": f"{group_type}objectgroup" 394 | } 395 | 396 | return self._create_instance(f'object/{group_type}groups', object_group) 397 | 398 | def create_net_group(self, name: str, objects: List[str], description: str = None) -> dict: 399 | """Creates a NetworkGroup object, containing at least 1 existing Network or NetworkGroup object 400 | 401 | :param name: Name of the NetworkGroup to be created 402 | :type name: str 403 | :param objects: Names of the Network or NetworkGroup objects to be added to the group 404 | :type objects: List[str] 405 | :param description: A description for the NetworkGroup, defaults to None 406 | :type description: str, optional 407 | :raises FirepyerResourceNotFound: If any of the given object names do not exist 408 | :return: The created NetworkGroup object 409 | :rtype: dict 410 | """ 411 | objects_for_group = [] 412 | for obj_name in objects: 413 | obj = self.get_net_obj_or_grp(obj_name) 414 | if obj: 415 | objects_for_group.append(obj) 416 | else: 417 | raise FirepyerResourceNotFound(f'Object "{obj_name}" not does not exist!') 418 | 419 | return self.create_group(name, 'network', objects_for_group, description) 420 | 421 | def delete_network_group(self, grp_id: str) -> bool: 422 | """Delete a NetworkGroup 423 | 424 | :param grp_id: NetworkGroup id 425 | :type grp_id: str 426 | :raises FirepyerResourceNotFound: If a NetworkGroup with the given id does not exist 427 | :return: True if the object is successfully deleted 428 | :rtype: bool 429 | """ 430 | return self._delete_instance('object/networkgroups', grp_id) 431 | 432 | def get_pending_changes(self) -> list: 433 | """Gets any configuration changes that have not yet been deployed 434 | 435 | :return: List of each change to be applied, empty list if there are none 436 | :rtype: list 437 | """ 438 | return self.get_paged_items('operational/pendingchanges') 439 | 440 | def deploy_now(self) -> str: 441 | """Starts a deployment, regardless of if there are any pending configuration changes 442 | 443 | :return: The ID for the Deployment task 444 | :rtype: str 445 | """ 446 | response = self.post_api('operational/deploy').json() 447 | return response.get('id') 448 | 449 | def get_deployment_status(self, deploy_id: str) -> str: 450 | """Gets the status of a Deployment task 451 | 452 | :param deploy_id: The ID of the Deployment task to check 453 | :type deploy_id: str 454 | :raises FirepyerResourceNotFound: If the deployment ID does not exist 455 | :return: The status of the deployment, one of either ['QUEUED', 'DEPLOYING', DEPLOYED', 'FAILED'] 456 | :rtype: str 457 | """ 458 | state = None 459 | response = self.get_api(f'operational/deploy/{deploy_id}') 460 | 461 | if response.status_code == 200: 462 | state = response.json().get('state') 463 | elif response.status_code == 404: 464 | raise FirepyerResourceNotFound(f'Resource with ID "{deploy_id}" not does not exist!') 465 | 466 | return state 467 | 468 | def deploy_config(self): 469 | """Checks if there's any pending config changes and deploys them, waits until deploy finishes to return 470 | 471 | :return: True if deployment was successful or not required, False if deployment failed 472 | :rtype: bool 473 | """ 474 | if self.get_pending_changes(): 475 | deployment_id = self.deploy_now() 476 | if deployment_id is None: 477 | # Unable to deploy 478 | return False 479 | state = self.get_deployment_status(deployment_id) 480 | while state != 'DEPLOYED' and state != 'FAILED': 481 | sleep(10) 482 | state = self.get_deployment_status(deployment_id) 483 | if state == 'DEPLOYED': 484 | return True 485 | elif state == 'FAILED': 486 | return False 487 | else: 488 | # Nothing to deploy 489 | return True 490 | 491 | def get_vrfs(self, name='', must_find: bool = False): 492 | """Gets all VRFs or a single VRF if a name is provided 493 | 494 | :param name: The name of a VRF to find, defaults to '' 495 | :type name: str, optional 496 | :param must_find: Specifies if an exception should be raised if the resource isn't found, defaults to False 497 | :type must_find: bool, optional 498 | :return: A list of all VRFs if no name is provided, or a dict of the single VRF with the given name 499 | :rtype: list|dict 500 | """ 501 | if name: 502 | return self.get_obj_by_name('devices/default/routing/virtualrouters', name, must_find=must_find) 503 | else: 504 | return self.get_api_items('devices/default/routing/virtualrouters') 505 | 506 | def get_bgp_general_settings(self): 507 | """Gets the device's general BGP settings if any are set 508 | 509 | :return: The BGPGeneralSettings object or None if none are set 510 | :rtype: dict 511 | """ 512 | return self.get_api_single_item('devices/default/routing/bgpgeneralsettings') 513 | 514 | def set_bgp_general_settings(self, asn: str, name='BgpGeneralSettings', description=None, router_id=None) -> dict: 515 | """Set the device's general BGP settings 516 | 517 | :param asn: The AS number for the BGP process 518 | :type asn: str 519 | :param name: A name for the settings, defaults to 'BgpGeneralSettings' 520 | :type name: str, optional 521 | :param description: A description for the settings, defaults to None 522 | :type description: str, optional 523 | :param router_id: A router ID for the BGP process, defaults to None 524 | :type router_id: str, optional 525 | :return: The BGPGeneralSettings object instance created 526 | :rtype: dict 527 | """ 528 | bgp_settings = {"name": name, 529 | "description": description, 530 | "asNumber": asn, 531 | "routerId": router_id, 532 | # "scanTime": 0, 533 | # "aggregateTimer": 0, 534 | # "bgpNextHopTriggerDelay": 0, 535 | # "bgpNextHopTriggerEnable": true, 536 | # "maxasLimit": 0, 537 | # "logNeighborChanges": true, 538 | # "transportPathMtuDiscovery": true, 539 | # "fastExternalFallOver": true, 540 | # "enforceFirstAs": true, 541 | # "asnotationDot": true, 542 | "type": "bgpgeneralsettings" 543 | } 544 | return self._create_instance('devices/default/routing/bgpgeneralsettings', bgp_settings) 545 | 546 | def get_bgp_settings(self, vrf='Global'): 547 | """Get the BGP settings for a specifc VRF or the default (Global) 548 | 549 | :param vrf: Name of a VRF to get the BGP settings, defaults to 'Global' 550 | :type vrf: str, optional 551 | :return: The BGPSettings object or None if none are set 552 | :rtype: dict 553 | """ 554 | vrf_id = self.get_vrfs(vrf)['id'] 555 | return self.get_api_single_item(f'devices/default/routing/virtualrouters/{vrf_id}/bgp') 556 | 557 | def set_bgp_settings(self, asn, name='', description=None, router_id=None, vrf='Global', af=4, auto_summary=False, 558 | neighbours=[], networks=[], default_originate=False) -> dict: 559 | """Configures BGP settings for the give (or default) VRF 560 | 561 | :param asn: The AS Number of the BGP process, MUST be the same as in the BGPGeneralSettings 562 | :type asn: str 563 | :param name: Name for the BGPSettings, MUST be unique across the device, defaults to 'VRFNAME-BGPSettings' 564 | :type name: str, optional 565 | :param description: Description for the BGPSettings, defaults to None 566 | :type description: str, optional 567 | :param router_id: A router ID for the BGP process, defaults to None 568 | :type router_id: str, optional 569 | :param vrf: Name of the VRF to configure BGP in, defaults to 'Global' 570 | :type vrf: str, optional 571 | :param af: BGP Address-Family to use, should be either [4, 6], defaults to 4 572 | :type af: int, optional 573 | :param auto_summary: Automatically summarise subnet routes to network routes, defaults to False 574 | :type auto_summary: bool, optional 575 | :param neighbours: Neighbours to add to the BGP process, each neighbour in the list should be a dict in format 576 | {"remoteAs": "65001", "activate": True, "ipv4Address": "192.168.1.1"}, defaults to [] 577 | :type neighbours: list, optional 578 | :param networks: Names of NetworkObjects to add to as networks into the BGP, defaults to [] 579 | :type networks: list, optional 580 | :param default_originate: Enable or disable default originate for BGP, defaults to False 581 | :type default_originate: bool, optional 582 | :return: The BGPSettings object instance created 583 | :rtype: dict 584 | """ 585 | if not name: 586 | name = f'{vrf}-BGPSettings' 587 | 588 | af_networks = [] 589 | af_neighbours = [] 590 | 591 | if af == 4: 592 | for network in networks: 593 | net_obj = self.get_net_objects(network) 594 | af_net = {"routeMap": {}, # TODO 595 | "ipv4Network": net_obj, 596 | "type": "afipv4network" 597 | } 598 | af_networks.append(af_net) 599 | 600 | for neighbour in neighbours: 601 | neighbour['type'] = 'neighboripv4' 602 | af_neighbours.append(neighbour) 603 | 604 | address_family = {"addressFamilyIPv4": { 605 | "autoSummary": auto_summary, 606 | "neighbors": af_neighbours, 607 | "networks": af_networks, 608 | "defaultInformationOrginate": default_originate, 609 | "type": "afipv4"} 610 | } 611 | elif af == 6: 612 | pass # TODO 613 | 614 | bgp_settings = {"name": name, 615 | "description": description, 616 | "asNumber": asn, 617 | "routerId": router_id, 618 | "type": "bgp" 619 | } 620 | bgp_settings.update(address_family) 621 | 622 | vrf_obj = self.get_vrfs(vrf, must_find=True) 623 | return self._create_instance(f'devices/default/routing/virtualrouters/{vrf_obj["id"]}/bgp', bgp_settings) 624 | 625 | def get_ospf_settings(self, vrf='Global') -> List[dict]: 626 | """Get the OSPF settings for a specifc VRF or the default (Global) 627 | 628 | :param vrf: Name of a VRF to get the OSPF settings, defaults to 'Global' 629 | :type vrf: str, optional 630 | :return: List of all OSPFSettings objects, one per process ID 631 | :rtype: List[dict] 632 | """ 633 | vrf_obj = self.get_vrfs(vrf, must_find=True) 634 | return self.get_api_items(f'devices/default/routing/virtualrouters/{vrf_obj["id"]}/ospf') 635 | 636 | def get_interfaces(self, name=''): 637 | """Gets all Interfaces or a single Interface if a name is provided 638 | 639 | :param name: The name of the Interface to find, defaults to '' 640 | :type name: str, optional 641 | :return: A list of all Interfaces if no name is provided, or a dict of the single Interface with the given name 642 | :rtype: list|dict 643 | """ 644 | if name: 645 | return self.get_obj_by_name('devices/default/interfaces', name) 646 | else: 647 | return self.get_api_items('devices/default/interfaces') 648 | 649 | def get_interface_by_phy(self, phy_name: str, must_find: bool = False) -> dict: 650 | """Get the dict for a Interface with the given physical name 651 | 652 | :param phy_name: The physical name of the Interface to find e.g. GigabitEthernet0/0 653 | :type phy_name: str 654 | :param must_find: Specifies if an exception should be raised if the resource isn't found, defaults to False 655 | :type must_find: bool, optional 656 | :return: Interface object is found, None if not 657 | :rtype: dict|None 658 | """ 659 | return self.get_class_by_name(self.get_interfaces(), phy_name, name_field_label='hardwareName', must_find=must_find) 660 | 661 | def get_subinterfaces(self, phy_name: str) -> List[dict]: 662 | """Gets all SubInterfaces for the given physical interface 663 | 664 | :param phy_name: The physical name of the Interface to find e.g. GigabitEthernet0/0 665 | :type phy_name: str 666 | :return: List of SubInterface objects found 667 | :rtype: List[dict] 668 | """ 669 | parent_interface = self.get_interface_by_phy(phy_name, must_find=True) 670 | return self.get_api_items(f'devices/default/interfaces/{parent_interface["id"]}/subinterfaces') 671 | 672 | def get_dhcp_servers(self) -> dict: 673 | """Gets the DHCP server configuration, including any pools 674 | 675 | :return: The DHCP server container object for all DHCP settings 676 | :rtype: dict 677 | """ 678 | return self.get_api_single_item('devicesettings/default/dhcpservercontainers') 679 | 680 | def delete_dhcp_server_pools(self): 681 | dhcp_server = self.get_dhcp_servers() 682 | dhcp_server['servers'] = [] 683 | return self.put_api(f'devicesettings/default/dhcpservercontainers/{dhcp_server["id"]}', 684 | data=json.dumps(dhcp_server)) 685 | 686 | def send_command(self, cmd: str): 687 | """Send a CLI command to the FTD device and return the output 688 | 689 | :param cmd: The full command to be sent to the CLI, abbreviations aren't supported 690 | :type cmd: str 691 | :return: The output from entering the command or None if the command failed 692 | :rtype: str 693 | """ 694 | cmd_body = {"commandInput": cmd, 695 | "type": "Command"} 696 | response = self.post_api('action/command', data=json.dumps(cmd_body)) 697 | if response.status_code == 200: 698 | return response.json()['commandOutput'] 699 | else: 700 | return None 701 | 702 | def get_port_groups(self, name=''): 703 | """Gets all PortGroups or a single PortGroup if a name is provided 704 | 705 | :param name: The name of a PortGroup to find, defaults to '' 706 | :type name: str, optional 707 | :return: A list of all PortGroups if no name is provided, or a dict of the single PortGroup with the given name 708 | :rtype: list|dict 709 | """ 710 | if name: 711 | return self.get_obj_by_name('object/portgroups?limit=0&', name) 712 | else: 713 | return self.get_api_items('object/portgroups?limit=0') 714 | 715 | def get_tcp_ports(self, name=''): 716 | """Gets all TCP type Ports or a single TCP Port object if a name is provided 717 | 718 | :param name: The name of a TCP Port to find, defaults to '' 719 | :type name: str, optional 720 | :return: A list of all TCP Ports if no name is provided, or a dict of the single TCP Port with the given name 721 | :rtype: list|dict 722 | """ 723 | if name: 724 | return self.get_obj_by_name('object/tcpports?limit=0&', name) 725 | else: 726 | return self.get_api_items('object/tcpports?limit=0') 727 | 728 | def get_udp_ports(self, name=''): 729 | """Gets all UDP type Ports or a single UDP Port object if a name is provided 730 | 731 | :param name: The name of a UDP Port to find, defaults to '' 732 | :type name: str, optional 733 | :return: A list of all UDP Ports if no name is provided, or a dict of the single UDP Port with the given name 734 | :rtype: list|dict 735 | """ 736 | if name: 737 | return self.get_obj_by_name('object/udpports?limit=0&', name) 738 | else: 739 | return self.get_api_items('object/udpports?limit=0') 740 | 741 | def get_icmp_ports(self, name='', af='4'): 742 | """Gets all ICMPv4/6 type Ports or a single ICMPv4/6 Port object if a name is provided 743 | 744 | :param name: The name of a ICMPv4 Port to find, defaults to '' 745 | :type name: str, optional 746 | :param af: Address family, '4' for an ICMPv4 object, '6' for an ICMPv6 object, defaults to '4' 747 | :type af: str, optional 748 | :return: A list of all ICMPv4 Ports if no name is provided, or a dict of the single ICMPv4 Port with the given name 749 | :rtype: list|dict 750 | """ 751 | if name: 752 | return self.get_obj_by_name(f'object/icmpv{af}ports?limit=0&', name) 753 | else: 754 | return self.get_api_items(f'object/icmpv{af}ports?limit=0') 755 | 756 | def create_icmp_port(self, name, type, code=None, af='4', description=None) -> dict: 757 | """Create an ICMPv4/6 Port object 758 | 759 | :param name: Name of the object 760 | :type name: str 761 | :param type: Must be a valid ICMPv4 or ICMPv6 type, see enum for options 762 | :type type: str 763 | :param code: Must be a valid ICMPv4 or ICMPv6 code, see enum for options, defaults to None 764 | :type code: str, optional 765 | :param af: Address family, '4' for an ICMPv4 object, '6' for an ICMPv6 object, defaults to '4' 766 | :type af: str, optional 767 | :param description: Description for the Port object, defaults to None 768 | :type description: str, optional 769 | :return: The ICMP Port object instance created 770 | :rtype: dict 771 | """ 772 | if code: 773 | code = code.upper() 774 | else: 775 | # Catches empty string 776 | code = None 777 | 778 | icmp_object = {'description': description, 779 | f'icmpv{af}Code': code, 780 | f'icmpv{af}Type': type.upper(), 781 | 'name': name, 782 | 'type': f'icmpv{af}portobject'} 783 | 784 | return self._create_instance(f'object/icmpv{af}ports', icmp_object) 785 | 786 | def get_port_obj_or_grp(self, name) -> dict: 787 | """Get a Port (tcp/udp/icmpv4/icmpv6) object or PortGroup by the given name 788 | 789 | :param name: Name of the object/group to find 790 | :type name: str 791 | :return: Single dict describing the object, if a resource with the name is found 792 | :rtype: dict 793 | """ 794 | port = None 795 | name_filter = {'name': name} 796 | port_finders = {self.get_tcp_ports: {}, 797 | self.get_udp_ports: {}, 798 | self.get_port_groups: {}, 799 | self.get_icmp_ports: {'af': '4'}, 800 | self.get_icmp_ports: {'af': '6'}} 801 | 802 | for get_port_method in port_finders: 803 | port_finders[get_port_method].update(name_filter) 804 | port = get_port_method(**port_finders[get_port_method]) 805 | if port: 806 | break 807 | return port 808 | 809 | def create_port_object(self, name: str, port: str, type: str, description: str = None) -> dict: 810 | """Create a TCP or UDP Port object to use in access rules 811 | 812 | :param name: Name of the Port object to be created 813 | :type name: str 814 | :param port: A single port number or '-' separated range of ports e.g. '80' or '8000-8008' 815 | :type port: str 816 | :param type: The protocol, must be one of ['tcp', 'udp'] 817 | :type type: str 818 | :param description: A description for the Port, defaults to None 819 | :type description: str, optional 820 | :return: The TCP/UDP Port object instance created 821 | :rtype: dict 822 | """ 823 | port_object = {"name": name, 824 | "description": description, 825 | "port": port, 826 | "type": f"{type.lower()}portobject" 827 | } 828 | return self._create_instance(f'object/{type.lower()}ports', port_object) 829 | 830 | def create_port_group(self, name: str, objects: List[str], description: str = None) -> dict: 831 | """Creates a PortGroup object, containing at least one existing tcp/udp/icmp Port or PortGroup 832 | 833 | :param name: Name of the PortGroup to create 834 | :type name: str 835 | :param objects: Names of the tcp/udp/icmp Port or PortGroup objects to be added to the group 836 | :type objects: List[str] 837 | :param description: A description for the PortGroup, defaults to None 838 | :type description: str, optional 839 | :return: The PortGroup object instance created 840 | :rtype: dict 841 | """ 842 | objects_for_group = [] 843 | for obj_name in objects: 844 | obj = self.get_port_obj_or_grp(obj_name) 845 | if obj: 846 | objects_for_group.append(obj) 847 | else: 848 | raise FirepyerResourceNotFound(f'Object "{obj_name}" not does not exist!') 849 | 850 | return self.create_group(name, 'port', objects_for_group, description) 851 | 852 | def get_initial_provision(self) -> dict: 853 | return self.get_api_single_item('devices/default/action/provision') 854 | 855 | def set_initial_provision(self, new_password: str, current_password: str = None): 856 | """Completes the out-of-box Initial Provisioning by accepting EULA and setting admin password 857 | 858 | :param new_password: The new admin password to set 859 | :type new_password: str 860 | :param current_password: The current admin password, if left as None self.password is used 861 | :type current_password: str, optional 862 | :return: The IntitialProvision object as a dict 863 | :rtype: dict 864 | """ 865 | if current_password is None: 866 | current_password = self.password 867 | provision = self.get_initial_provision() 868 | provision["acceptEULA"] = True 869 | provision["currentPassword"] = current_password 870 | provision["newPassword"] = new_password 871 | provision.pop('links') 872 | provision.pop('version') 873 | 874 | return self.post_api('devices/default/action/provision', 875 | data=json.dumps(provision)) 876 | 877 | def get_hostname_obj(self) -> dict: 878 | return self.get_api_single_item('devicesettings/default/devicehostnames') 879 | 880 | def get_hostname(self) -> str: 881 | """Get the hostname of the system 882 | 883 | :return: The hostname 884 | :rtype: str 885 | """ 886 | return self.get_hostname_obj()['hostname'] 887 | 888 | def set_hostname(self, hostname): 889 | """Sets the hostname of the system 890 | 891 | :param hostname: The hostname to set 892 | :type hostname: str 893 | :return: The full requests response object or None if an error occurred 894 | :rtype: Response 895 | """ 896 | current_hostname = self.get_hostname_obj() 897 | hostname_id = current_hostname['id'] 898 | new_hostname = {"hostname": hostname, 899 | "id": hostname_id, 900 | "version": current_hostname['version'], 901 | "type": "devicehostname"} 902 | 903 | return self.put_api(f'devicesettings/default/devicehostnames/{hostname_id}', 904 | data=json.dumps(new_hostname)) 905 | 906 | def get_upgrade_files(self) -> List[dict]: 907 | """Gets upgrade files that have been uploaded to the FTD appliance 908 | 909 | :return: List of upgrade file objects in dict form 910 | :rtype: List[dict] 911 | """ 912 | return self.get_api_items('managedentity/upgradefiles') 913 | 914 | def get_upgrade_file(self, file_id): 915 | return self.get_api(f'managedentity/upgradefiles/{file_id}').json() 916 | 917 | def upload_upgrade(self, filename: str) -> dict: 918 | """Uploads an FTD Upgrade file 919 | 920 | :param filename: Relative filepath and name of the FTD Upgrade tar file to upload 921 | :type filename: str 922 | :return: Uploaded file object 923 | :rtype: dict 924 | """ 925 | return self._upload_file(url='action/uploadupgrade', filename=filename) 926 | 927 | def perform_upgrade(self): 928 | return self.post_api('action/upgrade') 929 | 930 | def upload_vdb_file(self, filename: str) -> dict: 931 | """Uploads a Vulnerability Database (VDB) update file 932 | 933 | :param filename: Relative filepath and name of the VDB tar file to upload 934 | :type filename: str 935 | :return: Uploaded file object 936 | :rtype: dict 937 | """ 938 | return self._upload_file(url='action/updatevdbfromfile', filename=filename) 939 | 940 | def get_geolocation_update_jobs(self) -> dict: 941 | return self._get_rule_update_jobs('geolocation') 942 | 943 | def get_intrusion_rule_update_jobs(self) -> dict: 944 | return self._get_rule_update_jobs('sru') 945 | 946 | def get_vdb_update_jobs(self) -> dict: 947 | return self._get_rule_update_jobs('vdb') 948 | 949 | def _get_rule_update_jobs(self, rule_type: str) -> dict: 950 | return self.get_api_items(f'action/update{rule_type}') 951 | 952 | def upload_intrusion_rule_file(self, filename: str) -> dict: 953 | """Uploads an intrusion rule update (SRU) file 954 | 955 | :param filename: Relative filepath and name of the SRU tar file to upload 956 | :type filename: str 957 | :return: Uploaded file object 958 | :rtype: dict 959 | """ 960 | return self._upload_file(url='action/updatesrufromfile', filename=filename) 961 | 962 | def upload_geolocation_file(self, filename: str) -> dict: 963 | """Uploads a Geolocation Database (GeoDB) update file 964 | 965 | :param filename: Relative filepath and name of the GeoDB tar file to upload 966 | :type filename: str 967 | :return: Uploaded file object 968 | :rtype: dict 969 | """ 970 | return self._upload_file(url='action/updategeolocationfromfile', filename=filename) 971 | 972 | def _upload_file(self, url: str, filename: str) -> dict: 973 | # API parameter is called fileToUpload 974 | 975 | # Stream file using multipart, else may cause MemoryError if sending as whole file on some systems 976 | with open(filename, 'rb') as f: 977 | form = encoder.MultipartEncoder({ 978 | 'fileToUpload': (filename, f, 'application/octet-stream'), 979 | 'composite': 'NONE' 980 | }) 981 | headers = {'Prefer': 'respond-async', 'Content-Type': form.content_type} 982 | resp = self.post_api(uri=url, data=form, extra_headers=headers) 983 | return self._check_post_response(resp=resp, friendly_error='upload file') 984 | 985 | def update_vdb(self) -> dict: 986 | """Immediately update the Vulnerability Database (VDB) 987 | 988 | :return: VDB update job object 989 | :rtype: dict 990 | """ 991 | return self._update_rules('vdb') 992 | 993 | def update_intrusion_rules(self) -> dict: 994 | """Immediately update the intrusion ruleset (SRU) 995 | 996 | :return: Rule update job object 997 | :rtype: dict 998 | """ 999 | return self._update_rules('sru') 1000 | 1001 | def update_geolocation(self) -> dict: 1002 | """Immediately update the Geolocation Database (GeoDB) 1003 | 1004 | :return: GeoDB update job object 1005 | :rtype: dict 1006 | """ 1007 | return self._update_rules('geolocation') 1008 | 1009 | def _update_rules(self, rule_type: str): 1010 | job = {"type": f"{rule_type}updateimmediate"} 1011 | return self._create_instance(uri=f'action/update{rule_type}', instance_def=job, friendly_error=f'initiate {rule_type} update') 1012 | 1013 | def upload_config(self, filename: str) -> dict: 1014 | """Upload a JSON config file, usually a .txt or .zip previously exported from an FTD appliance 1015 | 1016 | :param filename: Relative filepath and name of the config file to upload 1017 | :type filename: str 1018 | :return: Uploaded file object 1019 | :rtype: dict 1020 | """ 1021 | return self._upload_file(url='action/uploadconfigfile', filename=filename) 1022 | 1023 | def download_config_file(self, remote_filename: str, local_filename: str = None) -> bool: 1024 | """Downloads a config file that has been exported (stored within FTD) 1025 | 1026 | :param remote_filename: Name of the config file on the FTD (diskFileName) or the export job ID 1027 | :type remote_filename: str 1028 | :param local_filename: Filename to save to the config file to locally, defaults to the remote filename 1029 | :type local_filename: str, optional 1030 | :raises FirepyerError: If unable to download the config file e.g. the filename does not exist or another error occurs 1031 | :return: True if the config is successfully downloaded 1032 | :rtype: bool 1033 | """ 1034 | response = self.get_api(f'action/downloadconfigfile/{remote_filename}', stream=True) 1035 | 1036 | if 'application/octet-stream' in response.headers.get('Content-Type'): 1037 | if not local_filename: 1038 | local_filename = remote_filename 1039 | with open(local_filename, "wb") as out_file: 1040 | for chunk in response.iter_content(chunk_size=512): 1041 | if chunk: 1042 | out_file.write(chunk) 1043 | return True 1044 | else: 1045 | raise FirepyerError('Failed to download config file! Check the file exists in FTD') 1046 | 1047 | def get_config_files(self) -> List[dict]: 1048 | """Gets the imported/exported config objects stored in FTD 1049 | 1050 | :return: List of each config file object 1051 | :rtype: List[dict] 1052 | """ 1053 | return self.get_api_items('action/configfiles') 1054 | 1055 | def delete_config_file(self, filename: str) -> bool: 1056 | """Deletes an exported/imported config file stored in FTD 1057 | 1058 | :param filename: Name of the config file object - "diskFileName" 1059 | :type filename: str 1060 | :raises FirepyerResourceNotFound: If a config with the given filename does not exist 1061 | :return: True if the file is successfully deleted 1062 | :rtype: bool 1063 | """ 1064 | return self._delete_instance('action/configfiles', filename) 1065 | 1066 | def apply_config_import(self, remote_filename: str, auto_deploy: bool = True) -> dict: 1067 | """Apply a JSON config file that has already been imported 1068 | 1069 | :param remote_filename: Filename of the config within the FTD system 1070 | :type remote_filename: str 1071 | :param auto_deploy: If the imported config should be deployed to the device or just sit in pending 1072 | :type auto_deploy: bool 1073 | :return: Config import job object 1074 | :rtype: dict 1075 | """ 1076 | import_job = {"type": "scheduleconfigimport", 1077 | "diskFileName": remote_filename, 1078 | "autoDeploy": auto_deploy} 1079 | return self._create_instance('action/configimport', instance_def=import_job) 1080 | 1081 | def get_config_imports(self, id: str = None): 1082 | if id: 1083 | return self.get_api(f'jobs/configimportstatus/{id}').json() 1084 | else: 1085 | return self.get_api_items('jobs/configimportstatus') 1086 | 1087 | def export_config(self, config_name: str = None) -> dict: 1088 | """Creates a job to save the current config as a JSON file in the FTD appliance. Once the job is complete the saved file can be downloaded 1089 | 1090 | :param config_name: Optional name to store the config file as, defaults to "Exported-at-YYYY-MM-DD-HH-MM-SSZ.zip" 1091 | :type config_name: str, optional 1092 | :return: Config export job object 1093 | :rtype: dict 1094 | """ 1095 | export_job = {"type": "scheduleconfigexport", 1096 | "diskFileName": config_name, 1097 | "doNotEncrypt": True, 1098 | "deployedObjectsOnly": True} 1099 | return self._create_instance('action/configexport', instance_def=export_job) 1100 | 1101 | def get_system_info(self) -> dict: 1102 | """Gets system information such as software versions, device model, serial number and management details 1103 | 1104 | :return: The target FTD system information 1105 | :rtype: dict 1106 | """ 1107 | return self.get_api('operational/systeminfo/default').json() 1108 | 1109 | def get_security_zones(self, name=''): 1110 | """Gets all SecurityZones or a single SecurityZone if a name is provided 1111 | 1112 | :param name: The name of the SecurityZone to find, defaults to '' 1113 | :type name: str, optional 1114 | :return: A list of all SecurityZones if no name is provided, or a dict of the single SecurityZone with the given name 1115 | :rtype: list|dict 1116 | """ 1117 | if name: 1118 | return self.get_obj_by_name('object/securityzones?limit=0&', name) 1119 | else: 1120 | return self.get_api_items('object/securityzones?limit=0') 1121 | 1122 | def create_security_zone(self, name, description='', interfaces=[], phy_interfaces=[], mode='ROUTED'): 1123 | """ 1124 | Creates a security zone 1125 | 1126 | :param name: str The name of the Security Zone 1127 | :param description: str Description 1128 | :param interfaces: list The logical names of any Interfaces to be part of this Security Zone e.g. inside 1129 | :param phy_interfaces: list The physical names of any Interfaces to be part of this Security Zone e.g. GigabitEthernet0/0 1130 | :param mode: str The mode of the Security Zone, either ROUTED or PASSIVE 1131 | """ 1132 | zone_interfaces = [] 1133 | 1134 | for intf in phy_interfaces: 1135 | intf_obj = self.get_interface_by_phy(intf) 1136 | zone_interfaces.append(intf_obj) 1137 | 1138 | for intf in interfaces: 1139 | intf_obj = self.get_interfaces(name=intf) 1140 | zone_interfaces.append(intf_obj) 1141 | 1142 | zone_object = {"name": name, 1143 | "description": description, 1144 | "interfaces": zone_interfaces, 1145 | "mode": mode.upper(), 1146 | "type": "securityzone" 1147 | } 1148 | return self.post_api('object/securityzones', json.dumps(zone_object)) 1149 | 1150 | def get_acp(self): 1151 | return self.get_api_single_item('policy/accesspolicies') 1152 | 1153 | def get_access_rules(self, name=''): 1154 | """Gets all AccessRules or a single AccessRule if a name is provided 1155 | 1156 | :param name: The name of the AccessRule to find, defaults to '' 1157 | :type name: str, optional 1158 | :return: A list of all AccessRules if no name is provided, or a dict of the single AccessRules with the given name 1159 | :rtype: list|dict 1160 | """ 1161 | policy_id = self.get_acp()['id'] 1162 | if name: 1163 | # Access rules cannot be filtered by name via the API so use get_class_by_name instead: 1164 | return self.get_class_by_name(self.get_access_rules(), name) 1165 | else: 1166 | return self.get_paged_items(f'policy/accesspolicies/{policy_id}/accessrules') 1167 | 1168 | def add_rule_item(self, item_name, item_obj, item_list): 1169 | if item_obj: 1170 | item_list.append(item_obj) 1171 | else: 1172 | raise FirepyerResourceNotFound(f'Resource with name "{item_name}" not does not exist!') 1173 | 1174 | def create_access_rule(self, name: str, action: str, src_zones: List[str] = [], src_networks: List[str] = [], src_ports: List[str] = [], 1175 | dst_zones: List[str] = [], dst_networks: List[str] = [], dst_ports: List[str] = [], int_policy: str = None, 1176 | syslog: str = None, log: str = '') -> dict: 1177 | """Create an AccessRule to be used in the main Access Policy. If any optional src/dst values are not 1178 | provided, they are treated as an 'any' 1179 | 1180 | :param name: Name of the AccessRule 1181 | :type name: str 1182 | :param action: The action the rule should take if matched, should be one of ['PERMIT', 'TRUST', 'DENY'] 1183 | :type action: str 1184 | :param src_zones: List of names of source Security Zones, defaults to [] 1185 | :type src_zones: list, optional 1186 | :param src_networks: List of names of source networks, names can be of either NetworkObject or NetworkGroup, defaults to [] 1187 | :type src_networks: list, optional 1188 | :param src_ports: List of names of source ports, names can be of either tcp/udp PortObject or PortGroup, defaults to [] 1189 | :type src_ports: list, optional 1190 | :param dst_zones: List of destination Security Zones, defaults to [] 1191 | :type dst_zones: list, optional 1192 | :param dst_networks: List of names of destination networks, names can be of either NetworkObject or NetworkGroup, defaults to [] 1193 | :type dst_networks: list, optional 1194 | :param dst_ports: List of names of destination ports, names can be of either tcp/udp PortObject or PortGroup, defaults to [] 1195 | :type dst_ports: list, optional 1196 | :param int_policy: Name of an IntrusionPolicy to apply to the rule, defaults to None 1197 | :type int_policy: str, optional 1198 | :param syslog: Name of a SyslogServer to log the rule to, in the format of IP:PORT, defaults to None 1199 | :type syslog: str, optional 1200 | :param log: Log the rule at start and end of connection, end of connection, or no log, should be one of ['BOTH', 'END', ''], defaults to '' 1201 | :type log: str, optional 1202 | :raises ResourceNotFound: If any of the object names passed in cannot be found e.g. a source network or dest port has not been created 1203 | :return: The AccessRule object that has been created 1204 | :rtype: dict 1205 | """ 1206 | 1207 | rule_src_zones = [] 1208 | rule_src_networks = [] 1209 | rule_src_ports = [] 1210 | rule_dst_zones = [] 1211 | rule_dst_networks = [] 1212 | rule_dst_ports = [] 1213 | rule_int_policy = None 1214 | rule_syslog = None 1215 | 1216 | for zone in src_zones: 1217 | z = self.get_security_zones(name=zone) 1218 | self.add_rule_item(zone, z, rule_src_zones) 1219 | 1220 | for network in src_networks: 1221 | net = self.get_net_obj_or_grp(network) 1222 | self.add_rule_item(network, net, rule_src_networks) 1223 | 1224 | for port in src_ports: 1225 | p = self.get_port_obj_or_grp(port) 1226 | self.add_rule_item(port, p, rule_src_ports) 1227 | 1228 | for zone in dst_zones: 1229 | z = self.get_security_zones(name=zone) 1230 | self.add_rule_item(zone, z, rule_dst_zones) 1231 | 1232 | for network in dst_networks: 1233 | net = self.get_net_obj_or_grp(network) 1234 | self.add_rule_item(network, net, rule_dst_networks) 1235 | 1236 | for port in dst_ports: 1237 | p = self.get_port_obj_or_grp(port) 1238 | self.add_rule_item(port, p, rule_dst_ports) 1239 | 1240 | if int_policy: 1241 | ip = self.get_intrusion_policies(int_policy) 1242 | if ip: 1243 | # Can't just pass the whole IntrusionPolicy object through like most others, so just pass the req'd fields 1244 | rule_int_policy = self._get_object_subset(ip) 1245 | 1246 | if syslog: 1247 | syslog_server = self.get_syslog_servers(syslog) 1248 | if syslog_server: 1249 | rule_syslog = syslog_server 1250 | 1251 | if log.upper() == 'BOTH': 1252 | log = 'LOG_BOTH' 1253 | elif log.upper() == 'END': 1254 | log = 'LOG_FLOW_END' 1255 | else: 1256 | log = 'LOG_NONE' 1257 | 1258 | rule = {"name": name, 1259 | "sourceZones": rule_src_zones, 1260 | "destinationZones": rule_dst_zones, 1261 | "sourceNetworks": rule_src_networks, 1262 | "destinationNetworks": rule_dst_networks, 1263 | "sourcePorts": rule_src_ports, 1264 | "destinationPorts": rule_dst_ports, 1265 | "ruleAction": action.upper(), 1266 | "eventLogAction": log, 1267 | "intrusionPolicy": rule_int_policy, 1268 | "syslogServer": rule_syslog, 1269 | "type": "accessrule" 1270 | } 1271 | 1272 | policy_id = self.get_acp()['id'] 1273 | return self._create_instance(f'policy/accesspolicies/{policy_id}/accessrules', rule) 1274 | 1275 | def delete_access_rule(self, rule_id: str) -> bool: 1276 | """Delete an AccessRule 1277 | 1278 | :param rule_id: AccessRule id 1279 | :type rule_id: str 1280 | :raises FirepyerResourceNotFound: If an AccessRule with the given id does not exist 1281 | :return: True if the object is successfully deleted 1282 | :rtype: bool 1283 | """ 1284 | policy_id = self.get_acp()['id'] 1285 | return self._delete_instance(f'policy/accesspolicies/{policy_id}/accessrules', rule_id) 1286 | 1287 | def get_smartlicense(self): 1288 | return self.get_api_items('license/smartlicenses') 1289 | 1290 | def set_smartlicense(self, license_type): 1291 | """ 1292 | Activates a SmartLicense of the given type 1293 | :param license_type: str The type of license to apply, must be one of ['BASE', 'MALWARE', 'THREAT', 'URLFILTERING', 'APEX', 'PLUS', 'VPNOnly'] 1294 | :return: dict The new license object, or the error message from the JSON response 1295 | """ 1296 | license_object = {"count": 1, 1297 | "compliant": True, 1298 | "licenseType": license_type, 1299 | "type": "license" 1300 | } 1301 | return self.post_api('license/smartlicenses', 1302 | data=json.dumps(license_object)).json() 1303 | 1304 | def get_smart_agent_connection(self): 1305 | """Gets the Smart License Agent Connection which will have a type of EVALUATION, REGISTER or UNIVERSAL_PLR 1306 | 1307 | :return: The SmartAgentConnection object in dict form 1308 | :rtype: dict 1309 | """ 1310 | return self.get_api_single_item('license/smartagentconnections') 1311 | 1312 | def set_smart_agent_connection(self, smart_agent_connection: dict, connection_type: str): 1313 | """Sets the Smart License Agent Connection type 1314 | 1315 | :param smart_agent_connection: A dict representation of the SmartAgentConnection object to modify 1316 | :type smart_agent_connection: dict 1317 | :param connection_type: The connection type to use, either "EVALUATION", "REGISTER" or "UNIVERSAL_PLR" 1318 | :type connection_type: str 1319 | :return: The updated SmartAgentConnection 1320 | :rtype: dict 1321 | """ 1322 | connection_type = connection_type.upper() 1323 | if connection_type not in ["EVALUATION", "REGISTER", "UNIVERSAL_PLR"]: 1324 | raise FirepyerInvalidOption('"connection_type" should be one of "EVALUATION", "REGISTER" or "UNIVERSAL_PLR"') 1325 | smart_agent_connection['connectionType'] = connection_type 1326 | 1327 | return self.put_api(f'license/smartagentconnections/{smart_agent_connection.get("id")}', 1328 | data=json.dumps(smart_agent_connection)).json() 1329 | 1330 | def get_plr_code(self): 1331 | """Generates a Universal PLR request code to be entered into Cisco licensing 1332 | 1333 | :return: A PLR request code object as a dict 1334 | :rtype: dict 1335 | """ 1336 | return self.get_api_single_item('license/operational/plrrequestcode') 1337 | 1338 | def install_plr_code(self, plr_code: str): 1339 | """Acitvates a Universal PLR license code gathered from Cisco licensing with a request code 1340 | 1341 | :param plr_code: PLR license code from Cisco 1342 | :type plr_code: str 1343 | :return: PLR install instance 1344 | :rtype: dict 1345 | """ 1346 | plr = {'code': plr_code, 1347 | 'type': 'PLRAuthorizationCode'} 1348 | return self._create_instance('license/action/installplrcode', plr, friendly_error='install PLR License') 1349 | 1350 | def get_intrusion_policies(self, name=''): 1351 | """Gets all IntrusionPolicies or a single IntrusionPolicy if a name is provided 1352 | 1353 | :param name: The name of the IntrusionPolicy to find, defaults to '' 1354 | :type name: str, optional 1355 | :return: A list of all IntrusionPolicies if no name is provided, or a dict of the single IntrusionPolicy with the given name 1356 | :rtype: list|dict 1357 | """ 1358 | if name: 1359 | return self.get_obj_by_name('policy/intrusionpolicies', name) 1360 | else: 1361 | return self.get_api_items('policy/intrusionpolicies') 1362 | 1363 | def get_syslog_servers(self, name=''): 1364 | """Gets all SyslogServers or a single SyslogServer if a name is provided 1365 | 1366 | :param name: The name of the SyslogServer to find. The name is stored in the format IP:PORT, defaults to '' 1367 | :type name: str, optional 1368 | :return: A list of all SyslogServers if no name is provided, or a dict of the single SyslogServer with the given name 1369 | :rtype: list|dict 1370 | """ 1371 | if name: 1372 | # Syslog server names are stored as IP:PORT, so unable to query using URL filter 1373 | return self.get_class_by_name(self.get_syslog_servers(), name) 1374 | else: 1375 | return self.get_api_items('object/syslogalerts?limit=0') 1376 | 1377 | def create_syslog_server(self, ip, protocol='UDP', port='514', interface=None) -> dict: 1378 | """Creates a SyslogServer to be able to send access rule and system logs to 1379 | 1380 | :param ip: IP address of the syslog server 1381 | :type ip: str 1382 | :param protocol: Protocol used to send syslog messages, must be one of ['TCP', 'UDP'], defaults to 'UDP' 1383 | :type protocol: str, optional 1384 | :param port: Port number used to send syslog messages, defaults to '514' 1385 | :type port: str, optional 1386 | :param interface: Name of a data interface to use as the source to reach the syslog server IP, otherwise mgmt will be used, defaults to None 1387 | :type interface: str, optional 1388 | :return: The new SyslogServer object 1389 | :rtype: dict 1390 | """ 1391 | use_mgmt = True 1392 | interface_object = None 1393 | 1394 | if interface: 1395 | use_mgmt = False 1396 | interface_object = self.get_interfaces(name=interface) 1397 | 1398 | syslog_object = {"deviceInterface": interface_object, 1399 | "useManagementInterface": use_mgmt, 1400 | "protocol": protocol.upper(), 1401 | "host": ip, 1402 | "port": port, 1403 | "type": "syslogserver" 1404 | } 1405 | return self._create_instance('object/syslogalerts', syslog_object) 1406 | 1407 | def get_users(self, name=''): 1408 | """Gets all Firepower Users or a single User object if a name is provided 1409 | 1410 | :param name: The name of a User to find, defaults to '' 1411 | :type name: str, optional 1412 | :return: A list of all Users if no name is provided, or a dict of the single User with the given name 1413 | :rtype: list|dict 1414 | """ 1415 | if name: 1416 | return self.get_obj_by_name('object/users', name) 1417 | else: 1418 | return self.get_api_items('object/users') 1419 | 1420 | def set_admin_password(self, new_password): 1421 | """Sets the pasword for the admin user of the system 1422 | 1423 | :param new_password: The new password to set for the user 1424 | :type new_password: str 1425 | :return: The full requests response object or None if an error occurred 1426 | :rtype: Response 1427 | """ 1428 | current_user = self.get_users('admin') 1429 | current_user['password'] = self.password 1430 | current_user['newPassword'] = new_password 1431 | 1432 | resp = self.put_api(f'object/users/{current_user["id"]}', 1433 | data=json.dumps(current_user)) 1434 | if resp.status_code == 200: 1435 | return True 1436 | elif resp.status_code == 422: 1437 | err_msgs = resp.json()['error'] 1438 | raise FirepyerError(f'Unable to set password due to the following error(s): {err_msgs}') 1439 | else: 1440 | raise FirepyerError() 1441 | -------------------------------------------------------------------------------- /firepyer/exceptions.py: -------------------------------------------------------------------------------- 1 | class FirepyerError(Exception): 2 | """Raised if no more-specific exception applies, such as errors presented directly from FTD 3 | """ 4 | def __init__(self, message) -> None: 5 | super().__init__(message) 6 | 7 | 8 | class FirepyerAuthError(FirepyerError): 9 | """Raised when the FTD rejects the username/password authentication 10 | """ 11 | pass 12 | 13 | 14 | class FirepyerInvalidOption(FirepyerError): 15 | """Raised when the value provided does not match the set of predefined options 16 | """ 17 | pass 18 | 19 | 20 | class FirepyerResourceNotFound(FirepyerError): 21 | """Raised when an object by a given name or ID cannot be found in the FTD config 22 | """ 23 | pass 24 | 25 | 26 | class FirepyerUnreachableError(FirepyerError): 27 | """Raised when the IP or hostname of the FTD device is unreachable 28 | """ 29 | pass 30 | -------------------------------------------------------------------------------- /firepyer/utils.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from itertools import zip_longest 3 | 4 | 5 | def read_objects_csv(filename): 6 | """Takes a CSV with headings and converts each row to a dict. Useful for populating create_network() 7 | 8 | :param filename: Full filename of the CSV 9 | :type filename: str 10 | :return: A list of dicts, each dict is a row from the CSV, with the heading as key and column as value 11 | :rtype: list 12 | """ 13 | objs = [] 14 | with open(filename) as objects_csv: 15 | objects_dict = csv.DictReader(objects_csv) 16 | for obj in objects_dict: 17 | objs.append(obj) 18 | return objs 19 | 20 | 21 | def read_groups_csv(filename): 22 | """Takes a CSV with headings in format: `name,objects,description` and converts each group to a dict. 23 | CSV file must be in hierarchical order if nested groups are to be created. Useful for populating create_group() 24 | 25 | :param filename: Full filename of the CSV 26 | :type filename: str 27 | :return: A list of dicts, each dict representing a group, with objects in the same group stored as list under the 'objects' key 28 | :rtype: list 29 | """ 30 | with open(filename) as objects_csv: 31 | objects_dict = csv.DictReader(objects_csv) 32 | return dicts_to_groups(objects_dict) 33 | 34 | 35 | def dicts_to_groups(objects_dict): 36 | groups = [] 37 | for obj in objects_dict: 38 | 39 | group_exists = False 40 | for group in groups: 41 | if group['name'] == obj['name']: 42 | group['objects'].append(obj['objects']) 43 | group_exists = True 44 | else: 45 | continue 46 | 47 | if not group_exists: 48 | group = {'name': obj['name'], 49 | 'objects': [obj['objects']], 50 | 'description': obj['description']} 51 | groups.append(group) 52 | return groups 53 | 54 | 55 | def expand_merged_csv(filename): 56 | """Takes a CSV that's had merged cells (to show a group in Excel) and fills the blanks with the previous row 57 | 58 | :param filename: Full filename of the CSV 59 | :type filename: str 60 | :return: A list of dicts, each dict is a row from the CSV, with the heading as key and column as value 61 | :rtype: list 62 | """ 63 | groups = [] 64 | with open(filename) as input_file: 65 | input_file = csv.reader(input_file) 66 | csv_headings = next(input_file) 67 | 68 | previous_row = [] 69 | for row in input_file: 70 | if any(row): 71 | row = [a or b for a, b in zip_longest(row, previous_row, fillvalue='')] 72 | previous_row = row 73 | 74 | group = {} 75 | for i, heading in enumerate(csv_headings): 76 | group[heading] = row[i] 77 | groups.append(group) 78 | return groups 79 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | requests-toolbelt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | from firepyer import __version__ 4 | 5 | with open("README.md", "r", encoding="utf-8") as fh: 6 | long_description = fh.read() 7 | 8 | setuptools.setup( 9 | name="firepyer", 10 | version=__version__, 11 | author="Marcus Cockerill", 12 | author_email="marcus@certa.network", 13 | license="Apache License 2.0", 14 | description="Interacting with Cisco FTD devices via the FDM REST API in Python", 15 | keywords='cisco firepower fdm ftd rest api python', 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | url="https://github.com/certanet/firepyer", 19 | packages=setuptools.find_packages(), 20 | install_requires=['requests', 'requests-toolbelt'], 21 | classifiers=[ 22 | 'Development Status :: 1 - Planning', 23 | 'Intended Audience :: Developers', 24 | "License :: OSI Approved :: Apache Software License", 25 | "Operating System :: OS Independent", 26 | 'Programming Language :: Python :: 3.8', 27 | 'Programming Language :: Python :: 3.9', 28 | ], 29 | python_requires='>=3.8', 30 | ) 31 | -------------------------------------------------------------------------------- /tests/test_fdm.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from firepyer import Fdm 4 | 5 | 6 | class TestFdm(unittest.TestCase): 7 | """Base class for testing firepyer Fdm methods 8 | """ 9 | def __init__(self, methodName: str) -> None: 10 | super().__init__(methodName=methodName) 11 | self.fdm = Fdm(host='192.168.45.45', username='admin', password='Admin123') 12 | -------------------------------------------------------------------------------- /tests/test_network.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from tests.test_fdm import TestFdm 4 | 5 | 6 | class TestNetwork(TestFdm): 7 | @patch('firepyer.requests.request') 8 | def test_get_network_objects(self, mock_request): 9 | """Test that the system-defined any-ipv4 NetworkObject exists 10 | """ 11 | mock_request.return_value.json.return_value = {'items': [{'name': 'any-ipv4'}]} 12 | group = self.fdm.get_net_objects('any-ipv4') 13 | 14 | self.assertEqual(group['name'], 'any-ipv4', "Should find default object") 15 | -------------------------------------------------------------------------------- /tests/test_system.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from tests.test_fdm import TestFdm 4 | 5 | 6 | class TestSystem(TestFdm): 7 | @patch('firepyer.requests.request') 8 | def test_system_info(self, mock_request): 9 | """Test system info 10 | """ 11 | mock_request.return_value.json.return_value = {'type': 'systeminformation'} 12 | response = self.fdm.get_system_info() 13 | 14 | self.assertEqual(response['type'], 'systeminformation') 15 | --------------------------------------------------------------------------------