├── docs ├── _static │ └── img │ │ ├── OpenVASreporting.png │ │ ├── screenshot-report.png │ │ ├── screenshot-report-h.png │ │ ├── screenshot-report-h1.png │ │ ├── screenshot-report-h2.png │ │ ├── screenshot-report1.png │ │ └── screenshot-report2.png ├── usage │ ├── index.rst │ ├── install.rst │ ├── export-csv.rst │ ├── export-csv-host.rst │ ├── export-csv-summary.rst │ ├── config-file.rst │ ├── export-excel.rst │ ├── export-excel-host.rst │ ├── export-word.rst │ ├── filters.rst │ └── export.rst ├── config-sample.yml ├── Makefile ├── changelog.rst ├── index.rst └── conf.py ├── openvasreporting ├── src │ ├── openvas-template.docx │ └── __init__.py ├── libs │ ├── __init__.py │ ├── parser.py │ ├── config.py │ ├── parsed_data.py │ └── export.py ├── __main__.py ├── __init__.py └── openvasreporting.py ├── setup.py ├── requirements.txt ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── codeql.yml │ ├── pythonpublish.yml │ └── stale.yml ├── .readthedocs.yaml ├── CHANGELOG ├── .gitignore ├── __init__.py ├── setup.cfg.bak ├── pyproject.toml ├── README.md └── LICENSE /docs/_static/img/OpenVASreporting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGroundZero/openvasreporting/HEAD/docs/_static/img/OpenVASreporting.png -------------------------------------------------------------------------------- /docs/_static/img/screenshot-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGroundZero/openvasreporting/HEAD/docs/_static/img/screenshot-report.png -------------------------------------------------------------------------------- /docs/_static/img/screenshot-report-h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGroundZero/openvasreporting/HEAD/docs/_static/img/screenshot-report-h.png -------------------------------------------------------------------------------- /docs/_static/img/screenshot-report-h1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGroundZero/openvasreporting/HEAD/docs/_static/img/screenshot-report-h1.png -------------------------------------------------------------------------------- /docs/_static/img/screenshot-report-h2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGroundZero/openvasreporting/HEAD/docs/_static/img/screenshot-report-h2.png -------------------------------------------------------------------------------- /docs/_static/img/screenshot-report1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGroundZero/openvasreporting/HEAD/docs/_static/img/screenshot-report1.png -------------------------------------------------------------------------------- /docs/_static/img/screenshot-report2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGroundZero/openvasreporting/HEAD/docs/_static/img/screenshot-report2.png -------------------------------------------------------------------------------- /openvasreporting/src/openvas-template.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheGroundZero/openvasreporting/HEAD/openvasreporting/src/openvas-template.docx -------------------------------------------------------------------------------- /docs/usage/index.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | Usage 3 | ***** 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | install 9 | export 10 | filters 11 | config-file 12 | -------------------------------------------------------------------------------- /openvasreporting/libs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 4 | # Project name: OpenVAS Reporting: A tool to convert OpenVAS XML reports into Excel files. 5 | # Project URL: https://github.com/TheGroundZero/openvasreporting 6 | -------------------------------------------------------------------------------- /openvasreporting/src/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 4 | # Project name: OpenVAS Reporting: A tool to convert OpenVAS XML reports into Excel files. 5 | # Project URL: https://github.com/TheGroundZero/openvasreporting 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | ### needs a setup.py so that readthedocs.io can successfully build 5 | ## 6 | # tip from https://github.com/readthedocs/readthedocs.org/issues/7030#issuecomment-624669745 7 | # 8 | # [...] And Brett Cannon (a Python core developer) gives the minimum setup.py file: 9 | # 10 | 11 | 12 | import setuptools 13 | 14 | if __name__ == "__main__": 15 | setuptools.setup() 16 | -------------------------------------------------------------------------------- /docs/config-sample.yml: -------------------------------------------------------------------------------- 1 | level: 2 | medium 3 | 4 | format: 5 | xlsx 6 | 7 | reporttype: 8 | host 9 | 10 | networks: 11 | includes: 12 | - 10.0.0.0/8 13 | - 172.16.0.0/12 14 | - 192.168.0.0/16 15 | excludes: 16 | - 172.16.168.234 17 | - 172.16.168.236-172.16.168.239 18 | - 172.20.16.120 19 | 20 | regex: 21 | excludes: 22 | - defender 23 | 24 | # I use this section to filter out recent ms patches not put in production yet. Or to filter in CVEs from the CISA Active Exploit bulletin 25 | cve: 26 | excludes: 27 | - CVE-2021-1971 28 | -------------------------------------------------------------------------------- /openvasreporting/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 4 | # Project name: OpenVAS Reporting: A tool to convert OpenVAS XML reports into Excel files. 5 | # Project URL: https://github.com/TheGroundZero/openvasreporting 6 | 7 | from .openvasreporting import main 8 | 9 | __author__ = 'TheGroundZero (https://github.com/TheGroundZero)' 10 | __maintainer__ = 'Eduardo Ferreira (@dudacgf)' 11 | 12 | if __name__ == '__main__': 13 | if __package__ is None: 14 | from os import sys, path 15 | 16 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 17 | del sys, path 18 | 19 | main() 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ## you don't need this file to build the package. but for development reasons... 2 | build>=0.7.0 3 | cycler>=0.11.0 4 | kiwisolver>=1.3 5 | defusedxml>=0.7.1 6 | matplotlib>=3.4.3 7 | netaddr>=0.8.0 8 | numpy>=1.21.4 9 | packaging>=21.2 10 | pep517>=0.12.0 11 | Pillow>=10.1.0 12 | pip>=21.3.1 13 | pyparsing>=2.4.7 14 | python-dateutil>=2.8.2 15 | python-docx>=0.8.11 16 | pyyaml>=6.0 17 | setuptools>=44.1.1 18 | six>=1.16.0 19 | tomli>=1.2.2 20 | wheel>=0.37.0 21 | XlsxWriter>=3.0.2 22 | fonttools>=4.43.0 # not directly required, pinned by Snyk to avoid a vulnerability 23 | zipp>=3.19.1 # not directly required, pinned to avoid CVE-2024-5569 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | # python: 21 | # install: 22 | # - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = OpenVASReporting 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) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | _A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]_ 12 | 13 | **Describe the solution you'd like** 14 | _A clear and concise description of what you want to happen._ 15 | 16 | **Describe alternatives you've considered** 17 | _A clear and concise description of any alternative solutions or features you've considered._ 18 | 19 | **Example code** 20 | _This could be pseudo-code explaining your idea_ 21 | 22 | ```python 23 | 24 | ``` 25 | 26 | **Additional context** 27 | _Add any other context or screenshots about the feature request here._ 28 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "1 11 * * 6" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v6 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v4 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v4 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v4 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | _A clear and concise description of what the bug is._ 12 | 13 | 14 | **Application usage** 15 | _How are you using the application?_ 16 | 17 | From repo / pip package / ... 18 | 19 | **Application version** 20 | _The version/release you're working with._ 21 | 22 | vX.X.X 23 | 24 | **Python version** 25 | _Version of your Python and Pip install_ 26 | 27 | ```bash 28 | python3 --version 29 | python3 -m pip --version 30 | ``` 31 | 32 | **Command + output** 33 | _What's the command you're trying to run and what's the output?_ 34 | 35 | ```bash 36 | openvasreporting -i openvasreport.xml -T host 37 | 38 | ... 39 | ``` 40 | 41 | **Expected behavior** 42 | A clear and concise description of what you expected to happen. 43 | 44 | 45 | **Additional context** 46 | Add any other context about the problem here. 47 | This could be screenshots, the XML of your report, ... 48 | -------------------------------------------------------------------------------- /docs/usage/install.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Installation 3 | ************ 4 | 5 | You can install this package directly from source by cloning the Git repository. 6 | 7 | .. code-block:: bash 8 | 9 | # Install pip 10 | apt(-get) install python3 python3-pip # Debian, Ubuntu 11 | yum -y install python3 python3-pip # CentOS 12 | dnf install python3 python3-pip # Fedora 13 | 14 | # Clone repo 15 | git clone git@github.com:TheGroundZero/openvasreporting.git 16 | 17 | # Install requirements 18 | cd openvasreporting 19 | pip3 install -r requirements.txt 20 | pip3 install build --upgrade 21 | pip3 install pip --upgrade 22 | python -m build . 23 | pip3 install dist/Openvas_Reporting[...].whl 24 | 25 | Alternatively, you can install the package through the Python package installer 'pip'. 26 | 27 | .. code-block:: bash 28 | 29 | # Install pip 30 | apt(-get) install python3 python3-pip # Debian, Ubuntu 31 | yum -y install python3 python3-pip # CentOS 32 | dnf install python3 python3-pip # Fedora 33 | 34 | # Install the package 35 | pip install OpenVAS-Reporting 36 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | release-build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v6 16 | 17 | - uses: actions/setup-python@v6 18 | with: 19 | python-version: '3.x' 20 | 21 | - name: Build release distributions 22 | run: | 23 | python -m pip install --upgrade pip build setuptools 24 | python -m build 25 | 26 | - name: Upload distributions 27 | uses: actions/upload-artifact@v5 28 | with: 29 | name: release-dists 30 | path: dist/ 31 | 32 | pypi-publish: 33 | runs-on: ubuntu-latest 34 | 35 | needs: 36 | - release-build 37 | 38 | permissions: 39 | id-token: write 40 | 41 | environment: 42 | name: pypi 43 | url: https://pypi.org/p/OpenVAS-Reporting/ 44 | 45 | steps: 46 | - name: Retrieve release distributions 47 | uses: actions/download-artifact@v6 48 | with: 49 | name: release-dists 50 | path: dist/ 51 | 52 | - name: Publish release distributions to PyPI 53 | uses: pypa/gh-action-pypi-publish@release/v1 -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '36 5 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v10 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | days-before-stale: 30 25 | days-before-close: 14 26 | stale-issue-message: 'This issue has been marked as stale. There has been no activity on this issue for 30 days. In 14 days this issue will be closed.' 27 | stale-pr-message: 'This PR has been marked as stale. There has been no activity on this Pull Request for 30 days. In 14 days this PR will be closed.' 28 | stale-issue-label: 'no-issue-activity' 29 | stale-pr-label: 'no-pr-activity' 30 | close-issue-message: 'This issue has been stale for too long. It has been closed.' 31 | close-pr-message: 'This Pull Request has been stale for too long. It has been closed.' 32 | remove-stale-when-updated: true 33 | exempt-issue-labels: bug, 'help wanted' 34 | -------------------------------------------------------------------------------- /docs/usage/export-csv.rst: -------------------------------------------------------------------------------- 1 | Export to Comma Separated Values 2 | -------------------------------- 3 | 4 | When passing the --format csv parameter, the tool will export reports in Comma Separated Values (CSV) format. 5 | The CSV format is optimized for import in Excel. 6 | 7 | Examples 8 | ^^^^^^^^ 9 | 10 | Create CSV report from 1 OpenVAS XML report using default settings 11 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 12 | 13 | .. code-block:: bash 14 | 15 | openvasreporting -i openvasreport.xml -f csv 16 | 17 | Create CSV report from multiple OpenVAS XML report using default settings 18 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 19 | 20 | .. code-block:: bash 21 | 22 | openvasreporting -i *.xml -f csv 23 | # OR 24 | openvasreporting -i openvasreport.xml -i openvasreport1.xml -i openvasreport2.xml [-i ...] -f csv 25 | 26 | Create CSV report from 1 OpenVAS XML report, reporting only severity level high and up 27 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 28 | 29 | .. code-block:: bash 30 | 31 | openvasreporting -i openvasreport.xml -o openvas_report -f csv -l h 32 | 33 | 34 | Result 35 | ^^^^^^ 36 | 37 | The final report will look similar to this: 38 | 39 | .. todo:: 40 | [DOCS] Add examples of CSV report 41 | 42 | Vulnerabilities are sorted according to CVSS score (descending) and vulnerability name (ascending). 43 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.6.1 - Fix PyPi upload 5 | 6 | 1.6.0 7 | - New Features: 8 | - Add Vulnerability version to report 9 | 10 | - Fixes: 11 | - Refactor code to help future developpement 12 | - Class, function and variable are now typed 13 | - Remove useless part of the script that where not used 14 | - Bump minimum Python version to 3.12 15 | 16 | 1.5.0 - Added args options to personnalize IP and CVE to keep/ignore 17 | 18 | 1.4.2 - Fixed "ValueError: Unknown format code 'f' for object of type 'str'" 19 | 20 | 1.4.1 - Small bugfixes and code refactoring 21 | 22 | 1.4.0 - Use Word template for report building 23 | 24 | 1.3.1 - Add charts to Word document using matplotlib. Some code clean-up and small lay-out changes in Excel. 25 | 26 | 1.3.0 - Fix retrieval of description and other useful info by parsing instead of 27 | 28 | 1.2.3 - Implement https://github.com/cr0hn/openvas_to_report/pull/12 29 | 30 | 1.2.2 - Fix bug where port info was not correctly extracted 31 | 32 | 1.2.1 - Fix bug where affected hosts were added on wrong row in Excel export 33 | 34 | 1.2.0 - Functional export to Word document (.docx). Includes some formatting. TODO: graphs 35 | 36 | 1.1.0a - Support for exporting to Word document (.docx). Limited formatting, needs more testing 37 | 38 | 1.0.1a - Small updates, preparing for export to other formats 39 | 40 | 1.0.0 - First official release, supports export to Excel with graphs, ToC and worksheet per vulnerability 41 | -------------------------------------------------------------------------------- /docs/usage/export-csv-host.rst: -------------------------------------------------------------------------------- 1 | Export to Comma Separated Values sorted by Host 2 | ----------------------------------------------- 3 | 4 | When passing the --format csv parameter, the tool will export reports in Comma Separated Values (CSV) format. If you use [-T host] parameter, the list will be sorted by host. 5 | 6 | The CSV format is optimized for import in Excel. 7 | 8 | Examples 9 | ^^^^^^^^ 10 | 11 | Create CSV report from 1 OpenVAS XML report, sorted by host, using default settings 12 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 13 | 14 | .. code-block:: bash 15 | 16 | openvasreporting -i openvasreport.xml -f csv -T host 17 | 18 | Create CSV report from multiple OpenVAS XML report, sorted by host, using default settings 19 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 20 | 21 | .. code-block:: bash 22 | 23 | openvasreporting -i *.xml -f csv -T host 24 | # OR 25 | openvasreporting -i openvasreport.xml -i openvasreport1.xml -i openvasreport2.xml [-i ...] -f csv -T host 26 | 27 | Create CSV report from 1 OpenVAS XML report, sorted by host and reporting only severity level high and up 28 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 29 | 30 | .. code-block:: bash 31 | 32 | openvasreporting -i openvasreport.xml -o openvas_report -f csv -l h -T host 33 | 34 | 35 | Result 36 | ^^^^^^ 37 | 38 | The final report will look similar to this: 39 | 40 | .. todo:: 41 | [DOCS] Add examples of CSV report 42 | 43 | Hosts are sorted according to number of CVSS score in each level (descending) 44 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.6.1 5 | 6 | - Fix PyPi upload 7 | 8 | 1.6.0 9 | 10 | - New Features 11 | - Add Vulnerability version to report 12 | - Fixes 13 | - Refactor code to help future developpement 14 | - Class, function and variable are now typed 15 | - Remove useless part of the script that where not used 16 | - Bump minimum Python to version 3.12 17 | 18 | 1.5.0 19 | 20 | - Added args options to personnalize IP and CVE to keep/ignore 21 | 22 | 1.4.2 23 | 24 | - Fixed `ValueError: Unknown format code 'f' for object of type 'str'` 25 | 26 | 1.4.1 27 | 28 | - Small bugfixes and code refactoring 29 | 30 | 1.4.0 31 | 32 | - Use Word template for report building 33 | 34 | 1.3.1 35 | 36 | - Add charts to Word document using matplotlib 37 | - Some code clean-up and small lay-out changes in Excel 38 | 39 | 1.3.0 40 | 41 | - Fix retrieval of description and other useful info by parsing `` instead of `` 42 | 43 | 1.2.3 44 | 45 | - Implement [https://github.com/cr0hn/openvas_to_report/pull/12] 46 | 47 | 1.2.2 48 | 49 | - Fix bug where port info was not correctly extracted 50 | 51 | 1.2.1 52 | 53 | - Fix bug where affected hosts were added on wrong row in Excel export 54 | 55 | 1.2.0 56 | 57 | - Functional export to Word document (.docx) 58 | - Includes some formatting 59 | - TODO: graphs 60 | 61 | 1.1.0a 62 | 63 | - Support for exporting to Word document (.docx) 64 | - Limited formatting, needs more testing 65 | 66 | 1.0.1a 67 | 68 | - Small updates, preparing for export to other formats 69 | 70 | 1.0.0 71 | 72 | - First official release, supports export to Excel with graphs, ToC and worksheet per vulnerability 73 | -------------------------------------------------------------------------------- /docs/usage/export-csv-summary.rst: -------------------------------------------------------------------------------- 1 | Export to Comma Separated Values sorted by Host 2 | ----------------------------------------------- 3 | 4 | When passing the --format csv parameter, the tool will export reports in Comma Separated Values (CSV) format. If you use [-T summary] parameter, an executive report will be generated. 5 | 6 | The report is suitable for later processing/inclusion in monitoring tools. 7 | 8 | The CSV format is optimized for import in Excel. 9 | 10 | Examples 11 | ^^^^^^^^ 12 | 13 | Create CSV report from 1 OpenVAS XML report, using default settings 14 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 15 | 16 | .. code-block:: bash 17 | 18 | openvasreporting -i openvasreport.xml -f csv -T summary 19 | 20 | Create CSV report from multiple OpenVAS XML report, using default settings 21 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 22 | 23 | .. code-block:: bash 24 | 25 | openvasreporting -i *.xml -f csv -T summary 26 | # OR 27 | openvasreporting -i openvasreport.xml -i openvasreport1.xml -i openvasreport2.xml [-i ...] -f csv -T summary 28 | 29 | Create CSV report from 1 OpenVAS XML report, and reporting only severity level high and up 30 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 31 | 32 | .. code-block:: bash 33 | 34 | openvasreporting -i openvasreport.xml -o openvas_report -f csv -l h -T summary 35 | 36 | 37 | Result 38 | ^^^^^^ 39 | 40 | The final report will look similar to this: 41 | 42 | .. code-block:: bash 43 | 44 | level,count,host_count 45 | critical,11,152 46 | high,19,71 47 | medium,22,134 48 | low,2,49 49 | none,0,0 50 | -------------------------------------------------------------------------------- /docs/usage/config-file.rst: -------------------------------------------------------------------------------- 1 | .YML Configuration File 2 | -------------------------------- 3 | 4 | You can use the **-c/--config-file** option to define a .yml file to be loaded with all the configuration to execute **openvasreporting** but input and output filenames. 5 | 6 | If you use this option, all other configuration options but input and output filenames will be ignored and if not defined in the .yml configuration file will be set to default. 7 | 8 | Sample Configuration File: 9 | """""""""""""""""""""""""" 10 | 11 | .. code-block:: yml 12 | 13 | level: 14 | medium 15 | 16 | format: 17 | xlsx 18 | 19 | reporttype: 20 | host 21 | 22 | networks: 23 | includes: 24 | - 10.0.0.0/8 25 | - 172.16.0.0/12 26 | - 192.168.0.0/16 27 | excludes: 28 | - 172.16.168.234 29 | - 172.16.168.236-172.16.168.239 30 | - 172.20.16.120 31 | 32 | regex: 33 | excludes: 34 | - defender 35 | 36 | # I use this section to filter out recent ms patches not put in production yet. Or to filter in CVEs from the CISA Active Exploit bulletin 37 | cve: 38 | excludes: 39 | - CVE-2021-1971 40 | 41 | Using this configuration file, the resulting report would be restricted to level medium and up, will be an Excel report, will be sorted by host, will filter in only rfc1918 local networks but will filter out some IP Ranges and IPs and will exclude CVE2021-1971. 42 | 43 | Examples 44 | ^^^^^^^^ 45 | 46 | Create report using above sample config: 47 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 48 | 49 | .. code-block:: bash 50 | 51 | openvasreporting -i openvasreport.xml -c ./config_defender_out_by_host.yml 52 | 53 | -------------------------------------------------------------------------------- /docs/usage/export-excel.rst: -------------------------------------------------------------------------------- 1 | Export to Excel 2 | --------------- 3 | 4 | By default (or when passing the --format xlsx parameter), the tool will export reports in Excel (xlsx) format. 5 | 6 | This report contains a summary sheet, table of contents, and a sheet per vulnerability containing vulnerability details 7 | and a list of affected hosts. 8 | 9 | Examples 10 | ^^^^^^^^ 11 | 12 | Create Excel report from 1 OpenVAS XML report using default settings 13 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 14 | 15 | .. code-block:: bash 16 | 17 | openvasreporting -i openvasreport.xml 18 | 19 | Create Excel report from multiple OpenVAS XML report using default settings 20 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 21 | 22 | .. code-block:: bash 23 | 24 | openvasreporting -i *.xml 25 | # OR 26 | openvasreporting -i openvasreport.xml -i openvasreport1.xml -i openvasreport2.xml [-i ...] 27 | 28 | Create Excel report from 1 OpenVAS XML report, reporting only severity level high and up 29 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 30 | 31 | .. code-block:: bash 32 | 33 | openvasreporting.py -i openvasreport.xml -o openvas_report -f xlsx -l h 34 | 35 | 36 | Result 37 | ^^^^^^ 38 | 39 | The final report will look similar to this: 40 | 41 | .. image:: ../_static/img/screenshot-report.png 42 | :alt: Report example screenshot - Summary 43 | :width: 30% 44 | 45 | .. image:: ../_static/img/screenshot-report1.png 46 | :alt: Report example screenshot - Table of Contents 47 | :width: 30% 48 | 49 | .. image:: ../_static/img/screenshot-report2.png 50 | :alt: Report example screenshot - Vulnerability description 51 | :width: 30% 52 | 53 | Vulnerability detail worksheets are sorted according to CVSS score and are colored according to the threat level. 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | .idea/ 3 | 4 | # Output File 5 | *.xlsx 6 | *.xml 7 | *.docx 8 | 9 | # Vscode settings 10 | .vscode/ 11 | 12 | # Mac cache 13 | *.DS_Store 14 | 15 | # Code coverage 16 | .coveragerc 17 | codecov.yml 18 | 19 | # Byte-compiled / optimized / DLL files 20 | __pycache__/ 21 | *.py[cod] 22 | *$py.class 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | build/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | .eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | wheels/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | .hypothesis/ 66 | .pytest_cache/ 67 | 68 | # Translations 69 | *.mo 70 | *.pot 71 | 72 | # Django stuff: 73 | *.log 74 | local_settings.py 75 | db.sqlite3 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | 124 | # wings projects 125 | *.wpr 126 | 127 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 4 | # Project name: OpenVAS to Report: A tool to convert OpenVAS XML reports into Excel files. 5 | # Project URL: https://github.com/TheGroundZero/openvasreporting 6 | # 7 | # Copyright 8 | # This project is based on OpenVAS2Report (https://github.com/cr0hn/openvas_to_report) (c) 2015, cr0hn<-AT->cr0hn.com 9 | # All rights reserved. 10 | # 11 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 12 | # following conditions are met: 13 | # 14 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the 15 | # following disclaimer. 16 | # 17 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 18 | # following disclaimer in the documentation and/or other materials provided with the distribution. 19 | # 20 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 21 | # products derived from this software without specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 24 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | # 31 | 32 | 33 | # 34 | # This file exists only so that I can debug using Wings. I don't know why (@dudacgf) 35 | # 36 | 37 | __author__ = 'TheGroundZero (https://github.com/TheGroundZero)' 38 | __maintainer__ = 'Eduardo Ferreira (@dudacgf)' 39 | __package__ = str("openvasreporting") 40 | -------------------------------------------------------------------------------- /setup.cfg.bak: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = openvasreporting 3 | version = 1.6.1 4 | description = A tool to convert OpenVAS XML into reports. 5 | long_description = file: README.md, LICENSE, CHANGELOG 6 | long_description_content_type = text/markdown 7 | 8 | author = TheGroundZero (@DezeStijn) 9 | author_email = 2406013+TheGroundZero@users.noreply.github.com 10 | maintainer = Eduardo Ferreira (@dudacgf) 11 | #maintainer_email = does-not-exist-yet@nowhere-to-be-found 12 | url = https://github.com/TheGroundZero/openvasreporting 13 | project_urls = 14 | Source Code = https://github.com/TheGroundZero/openvasreporting 15 | Documentation = https://openvas-reporting.sequr.be 16 | Issues = https://github.com/TheGroundZero/openvasreporting/issues/ 17 | 18 | license = GPL-3.0-or-later 19 | keywords = OpenVAS OpenVAS-reports Excel xlsxwriter xlsx reporting reports report 20 | classifiers = 21 | Development Status :: 5 - Production/Stable 22 | Environment :: Console 23 | Framework :: Sphinx 24 | Intended Audience :: Developers 25 | Intended Audience :: End Users/Desktop 26 | Intended Audience :: Information Technology 27 | Intended Audience :: Other Audience 28 | Intended Audience :: System Administrators 29 | License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) 30 | Natural Language :: English 31 | Operating System :: MacOS 32 | Operating System :: Microsoft :: Windows 33 | Operating System :: POSIX 34 | Operating System :: Unix 35 | Programming Language :: Python 36 | Topic :: Documentation :: Sphinx 37 | Topic :: Internet :: Log Analysis 38 | Topic :: Security 39 | 40 | [options] 41 | include_package_data = True 42 | packages = find: 43 | install_requires = 44 | xlsxwriter>=1.0.0 45 | python-docx>=0.8.7 46 | matplotlib>=2.2.2 47 | netaddr>=0.8.0 48 | python_requires = >=3.12 49 | 50 | [options.packages.find] 51 | include = 52 | openvasreporting 53 | openvasreporting.libs 54 | 55 | [options.entry_points] 56 | console_scripts = 57 | openvasreporting = openvasreporting:main 58 | 59 | [options.package_data] 60 | #* = openvasreporting/src/openvas-template.docx 61 | openvasreporting = src/openvas-template.docx 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /openvasreporting/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 4 | # Project name: OpenVAS to Report: A tool to convert OpenVAS XML reports into Excel files. 5 | # Project URL: https://github.com/TheGroundZero/openvasreporting 6 | # 7 | # Copyright 8 | # This project is based on OpenVAS2Report (https://github.com/cr0hn/openvas_to_report) (c) 2015, cr0hn<-AT->cr0hn.com 9 | # All rights reserved. 10 | # 11 | # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 12 | # following conditions are met: 13 | # 14 | # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the 15 | # following disclaimer. 16 | # 17 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 18 | # following disclaimer in the documentation and/or other materials provided with the distribution. 19 | # 20 | # 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote 21 | # products derived from this software without specific prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 24 | # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | # 31 | 32 | __author__ = 'TheGroundZero (https://github.com/TheGroundZero)' 33 | __maintainer__ = 'Eduardo Ferreira (@dudacgf)' 34 | __package__ = str("openvasreporting") 35 | 36 | ## import main here so shell script execution finds the egg 37 | from .openvasreporting import main, convert 38 | from .libs.config import Config, Config_YAML 39 | 40 | -------------------------------------------------------------------------------- /docs/usage/export-excel-host.rst: -------------------------------------------------------------------------------- 1 | Export to Excel sorted by Host 2 | ------------------------------ 3 | 4 | By default (or when passing the --format xlsx parameter), the tool will export reports in Excel (xlsx) format sorted by vulnerability. If you add the **--report-type host** parameter, it will generate an Excel report sorted by Host. 5 | 6 | This report contains a summary sheet, table of contents, and a sheet per Host containing vulnerability details. 7 | 8 | Examples 9 | ^^^^^^^^ 10 | 11 | Create Excel report from 1 OpenVAS XML report, sorted by host, using default settings 12 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 13 | 14 | .. code-block:: bash 15 | 16 | openvasreporting -i openvasreport.xml -T host 17 | 18 | Create Excel report from multiple OpenVAS XML report, sorted by host, using default settings 19 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 20 | 21 | .. code-block:: bash 22 | 23 | openvasreporting -i *.xml -T host 24 | # OR 25 | openvasreporting -i openvasreport.xml -i openvasreport1.xml -i openvasreport2.xml [-i ...] -T HOST 26 | 27 | Create Excel report from 1 OpenVAS XML report, sorted by host, reporting only severity level high and up 28 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 29 | 30 | .. code-block:: bash 31 | 32 | openvasreporting -i openvasreport.xml -o openvas_report -f xlsx -l h -T HOST 33 | 34 | 35 | Result 36 | ^^^^^^ 37 | 38 | The final report will look similar to this: 39 | 40 | .. image:: ../_static/img/screenshot-report-h.png 41 | :alt: Report example screenshot - Summary 42 | :width: 30% 43 | 44 | .. image:: ../_static/img/screenshot-report-h1.png 45 | :alt: Report example screenshot - Table of Contents 46 | :width: 30% 47 | 48 | .. image:: ../_static/img/screenshot-report-h2.png 49 | :alt: Report example screenshot - Vulnerability description 50 | :width: 30% 51 | 52 | Host Ranking and TOC is sorted according to the maximum CVSS score of a host followed by number of entries at each threat level. 53 | 54 | Host worksheets are sorted by CVSS score followed by vulnerability name. 55 | 56 | All worksheets are colored according to threat level. 57 | 58 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "openvasreporting" 7 | dependencies = [ 8 | "xlsxwriter>=1.0.0", 9 | "python-docx>=0.8.7", 10 | "matplotlib>=2.2.2", 11 | "netaddr>=0.8.0" 12 | ] 13 | requires-python = ">= 3.12" 14 | authors = [ 15 | {name = "TheGroundZero", email = "2406013+TheGroundZero@users.noreply.github.com"} 16 | ] 17 | maintainers = [ 18 | {name = "BEduardo Ferreira (@dudacgf)"} 19 | ] 20 | description = "A tool to convert OpenVAS XML into reports." 21 | readme = {file = "README.md", content-type = "text/markdown"} 22 | license = {file = "LICENSE"} 23 | keywords = ["OpenVAS", "OpenVAS-reports", "Excel", "xlsxwriter", "xlsx", "reporting", "reports", "report"] 24 | classifiers = [ 25 | "Development Status :: 5 - Production/Stable", 26 | "Environment :: Console", 27 | "Framework :: Sphinx", 28 | "Intended Audience :: Developers", 29 | "Intended Audience :: End Users/Desktop", 30 | "Intended Audience :: Information Technology", 31 | "Intended Audience :: Other Audience", 32 | "Intended Audience :: System Administrators", 33 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 34 | "Natural Language :: English", 35 | "Operating System :: MacOS", 36 | "Operating System :: Microsoft :: Windows", 37 | "Operating System :: POSIX", 38 | "Operating System :: Unix", 39 | "Programming Language :: Python", 40 | "Topic :: Documentation :: Sphinx", 41 | "Topic :: Internet :: Log Analysis", 42 | "Topic :: Security" 43 | ] 44 | dynamic = ["version"] 45 | 46 | [project.urls] 47 | Repository = "https://github.com/TheGroundZero/openvasreporting" 48 | Documentation = "https://openvas-reporting.sequr.be" 49 | Issues = "https://github.com/TheGroundZero/openvasreporting/issues/" 50 | Changelog = "https://github.com/TheGroundZero/openvasreporting/blob/main/CHANGELOG" 51 | 52 | [project.scripts] 53 | openvasreporting = "openvasreporting:main" 54 | 55 | [tool.setuptools] 56 | include-package-data = true 57 | 58 | [tool.setuptools.package-data] 59 | openvasreporting = ["src/openvas-template.docx"] 60 | 61 | [tool.setuptools.packages.find] 62 | include = ["openvasreporting", "openvasreporting.libs"] 63 | -------------------------------------------------------------------------------- /docs/usage/export-word.rst: -------------------------------------------------------------------------------- 1 | Export to Word 2 | -------------- 3 | 4 | When passing the --format docx parameter, the tool will export reports in Word (docx) format. 5 | 6 | This report contains a summary sheet, table of contents, and a sheet per vulnerability containing vulnerability details 7 | and a list of affected hosts. 8 | 9 | Examples 10 | ^^^^^^^^ 11 | 12 | Create Word report from 1 OpenVAS XML report using default settings 13 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 14 | 15 | .. code-block:: bash 16 | 17 | openvasreporting -i openvasreport.xml -f docx 18 | 19 | Create Word report from multiple OpenVAS XML report using default settings 20 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 21 | 22 | .. code-block:: bash 23 | 24 | openvasreporting -i *.xml -f docx 25 | # OR 26 | openvasreporting -i openvasreport.xml -i openvasreport1.xml -i openvasreport2.xml [-i ...] -f docx 27 | 28 | Create Word report from 1 OpenVAS XML report, reporting only severity level high and up 29 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 30 | 31 | .. code-block:: bash 32 | 33 | openvasreporting -i openvasreport.xml -o openvas_report -f docx -l h 34 | 35 | Create Word report using a different template 36 | """"""""""""""""""""""""""""""""""""""""""""" 37 | 38 | .. code-block:: bash 39 | 40 | openvasreporting -i openvasreport.xml -o openvas_report -f docx -t /home/user/myOpenvasTemplate.docx 41 | 42 | The custom template document must contain a definition for the following styles: 43 | 44 | - Title (default) 45 | - Heading 1 (default) 46 | - Heading 4 (default) 47 | - OV-H1toc (custom format for Heading 1, included in Table of Contents) 48 | - OV-H2toc (custom format for Heading 2, included in Table of Contents) 49 | - OV-Finding (custom format for finding titles, included in Table of Contents) 50 | 51 | To modify these styles, use the style selector in your prefered document editor (e.g. MS Office Word, LibreOffice, ...). 52 | See also `this issue on GitHub`_. 53 | 54 | Result 55 | ^^^^^^ 56 | 57 | The final report will look similar to this: 58 | 59 | .. todo:: 60 | [DOCS] Add screenshots of Word report 61 | 62 | Vulnerabilities are sorted according to CVSS score (descending) and vulnerability name (ascending). 63 | 64 | .. _this issue on GitHub: https://github.com/TheGroundZero/openvasreporting/issues/11#issuecomment-578644876 65 | -------------------------------------------------------------------------------- /docs/usage/filters.rst: -------------------------------------------------------------------------------- 1 | Using Filters 2 | ------------- 3 | 4 | You can filter the vulnerabilities that will be presented in you report using one of the filtering options. You can filter: 5 | - networks cidrs, ip ranges and any individual ip using the options **-n/--network-include** and **-N/--network-exclude**; 6 | - regex expressions that will be matched against the vulnerability names using the options **-r/--regex-include** and **-R/--regex-exclude** - The matches will be case insensitive; 7 | - CVEs numbers in the format CVEYYYY-nnn... using the options **-e/--cve-include** and **-E/--cve-exclude**; 8 | When passing the --format csv parameter, the tool will export reports in Comma Separated Values (CSV) format. 9 | 10 | All these options receive the path to a .txt file containing one filtering option by line. 11 | 12 | 13 | Examples 14 | ^^^^^^^^ 15 | 16 | Create xlsx report from multiple OpenVAS XML Report filtering by network 17 | """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 18 | 19 | .. code-block:: bash 20 | 21 | openvasreporting -i *.xml -n ./branch_1.txt -N ./branch_1_ipaliases.txt 22 | 23 | Contents of *branch_1.txt* could be: 24 | 25 | .. code-block:: txt 26 | 27 | 172.16.168.0/24 28 | 172.16.0.1-172.16.0.3 29 | 172.16.1.1 30 | 31 | Contents of *branch_1_ipaliases.txt* could be: 32 | 33 | .. code-block:: txt 34 | 35 | 172.16.168.234 36 | 172.16.168.236-239 37 | 172.16.168.15 38 | 39 | Create xlsx report, sorted by host, filtering by regex 40 | """""""""""""""""""""""""""""""""""""""""""""""""""""" 41 | 42 | .. code-block:: bash 43 | 44 | openvasreporting -i *.xml -T host -R ./regex_defender.txt 45 | 46 | Contents of *regex_defender.txt* could be: 47 | 48 | .. code-block:: txt 49 | 50 | defender 51 | 52 | Create xlss report from 1 OpenVAS XML report, filtering by CVE 53 | """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 54 | 55 | .. code-block:: bash 56 | 57 | openvasreporting -i openvasreport.xml -e ./cisa_nov_2021.txt 58 | 59 | Contents of *cisa_nov_2021.txt* could be 60 | 61 | .. code-block:: txt 62 | 63 | CVE-2021-27104 64 | CVE-2021-27102 65 | CVE-2021-27101 66 | [...] 67 | CVE-2020-10189 68 | CVE-2019-8394 69 | CVE-2020-29583 70 | 71 | Of course, you can mix filtering options: 72 | """"""""""""""""""""""""""""""""""""""""" 73 | 74 | .. code-block:: bash 75 | 76 | openvasreporting -i *.xml -r ./regex_defender.txt -e ./cisa_nov_2021.txt 77 | 78 | -------------------------------------------------------------------------------- /docs/usage/export.rst: -------------------------------------------------------------------------------- 1 | Command line usage 2 | ================== 3 | 4 | .. code-block:: bash 5 | 6 | # When working from the Git repo 7 | python3 -m openvasreporting -i *.xml [-i ...] [-c config.yml] [-o openvas_report] [-f xlsx] [-l none] [-t "openvasreporting/src/openvas-template.docx"] [-T vulnerability] [-n included_networks] [-N excluded-networks] [-r included-regex] [-R excluded-regex] [-e included-cve] [-E excluded-cve] 8 | # When using the pip package 9 | openvasreporting -i *.xml [-i ...] [-c config.yml] [-o openvas_report] [-f xlsx] [-l none] [-t "openvasreporting/src/openvas-template.docx"] [-T vulnerability] [-n included_networks] [-N excluded-networks] [-r included-regex] [-R excluded-regex] [-e included-cve] [-E excluded-cve] 10 | 11 | \-i, --input 12 | | Mandatory 13 | | Selects the OpenVAS XML report file(s) to be used as input. 14 | | Accepts one or more inputs, including wildcards 15 | 16 | \-o, --output 17 | | Optional 18 | | Name of the output file, without extension. 19 | | Defaults to: openvas_report 20 | 21 | \-c, --config-file 22 | | Optional 23 | | Path to a .yml file containing the configuration (format, level, type, filters) 24 | | If this option is used all other options (but input and output files) will be ignored 25 | | Defaults to: None 26 | 27 | \-f, --format 28 | | Optional 29 | | Type of output file. 30 | | Valid values are: xlsx, docx, csv 31 | | Defaults to: xlsx 32 | 33 | \-l, --level 34 | | Optional 35 | | Minimal severity level of finding before it's included in the report. 36 | | Valid values are: c(ritical), h(igh), m(edium), l(low), n(one) 37 | | Defaults to: none 38 | 39 | \-t, --template 40 | | Optional, only used with '-f docx' 41 | | Template document for docx export. Document must contain formatting for styles used in export. 42 | | Valid values are: path to a docx file 43 | | Defaults to: openvasreporting/src/openvas-template.docx 44 | 45 | \-T, --report-type 46 | | Optional 47 | | Selects if will list hosts by vulnerability (v) or vulnerabilities by host (h) 48 | | Valid values are: v, h, vulnerabiity, host 49 | | Defaults to: vulnerability 50 | 51 | \-e, --network-include 52 | | Optional 53 | | path to a file containing a list of ips, ipcidrs or ipaddrs (one per line) that 54 | | will be included in the report 55 | | Defaults to: all hosts with appropriate level will be included 56 | 57 | 58 | \-E, --network-exclude 59 | | Optional 60 | | path to a file containing a list of ips, ipcidrs or ipaddrs (one per line) that 61 | | will be excluded from the report 62 | | Defaults to: no excluded hosts 63 | 64 | \-r, --regex-include 65 | | Optional 66 | | path to a file containing a list of regex expressions that will be matched against 67 | | the name of the vulnerability field to be filtered into the report 68 | | Defaults to: all vulnerabilities will be included 69 | 70 | \-R, --regex-exclude 71 | | Optional 72 | | path to a file containing a list of regex expressions that will be matched against 73 | | the name of the vulnerability field to be filtered out of the report 74 | | Defaults to: no excluded vulnerabilities 75 | 76 | \-e, --cve-include 77 | | Optional 78 | | path to a file containing a list of CVEs (format CVEYYYY-nnn...) that will be 79 | | filtered into the report 80 | | Defaults to: all vulnerabilities with -l level will be included 81 | 82 | \-C, --cve-exclude 83 | | Optional 84 | | path to a file containing a list of CVEs (format CVEYYYY-nnn...) that will be 85 | | filtered out of the report 86 | | Defaults to: no excluded hosts 87 | 88 | 89 | .. todo:: 90 | [Feature] Export to other formats (PDF, [proper] CSV) 91 | 92 | 93 | .. toctree:: 94 | :caption: Export formats 95 | :maxdepth: 1 96 | 97 | export-excel 98 | export-word 99 | export-csv 100 | export-excel-host 101 | export-csv-host 102 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | OpenVAS Reporting 2 | ================= 3 | 4 | |version| |license| |docs| |pypi_version| |pypi_format| 5 | |vulns| |codecov| 6 | 7 | A tool to convert OpenVAS XML into reports. 8 | 9 | Table of contents 10 | ----------------- 11 | 12 | .. toctree:: 13 | :maxdepth: 3 14 | 15 | Welcome 16 | changelog 17 | usage/index 18 | 19 | What's OpenVAS Reporting? 20 | ------------------------- 21 | 22 | OpenVAS reporting allows you to create a report from one or more OpenVAS/Greenbone XML reports. 23 | This way, it's easy to create simple graphs for the compliance department, create pivot tables to collect statistics, 24 | or combine multiple scan reports into one. 25 | 26 | The tool currently only supports exports to Excel, creating a workbook containing a summary sheet, table of contents 27 | and a worksheet per vulnerability (with info and list of hosts). 28 | 29 | .. image:: _static/img/screenshot-report.png 30 | :alt: Report example screenshot - Summary 31 | :width: 30% 32 | 33 | .. image:: _static/img/screenshot-report1.png 34 | :alt: Report example screenshot - Table of Contents 35 | :width: 30% 36 | 37 | .. image:: _static/img/screenshot-report2.png 38 | :alt: Report example screenshot - Vulnerability description 39 | :width: 30% 40 | 41 | Why create this tool? 42 | --------------------- 43 | 44 | OpenVAS is an awesome tool for many people and it's UI is nice but not always intuitive. It currently also lacks the 45 | ability to merge multiple task reports into one, especially when testing multiple environments. This tool allows you to 46 | merge multiple XML reports into one. 47 | 48 | Working with CSV exports of the OpenVAS reports is just a pain, since they include newlines which cause Excel (or other 49 | importers) to view those lines as new inputs. This can be fixed with some find/replace voodoo, which can probably be 50 | automated as well (in tools like Notepad++), but it's just too much hassle to filter out all edge cases. 51 | 52 | Or maybe you just prefer working in Excel because you're used to it 53 | (Excel is probably the #1 IT tool across all branches). 54 | 55 | Aren't there other tools to achieve this? 56 | ----------------------------------------- 57 | 58 | As a good netizen, I used some search engine terms to find tools that would fit my needs (merge reports, export to Excel, 59 | perhaps export to other formats) and found cr0hn's `OpenVAS2Report `_. 60 | However, it appears that either the format of the XML reports has changed and the code doesn't handle this well, or my 61 | reports contain some weird voodoo, because some of my reports would convert (partially) while others wouldn't. 62 | 63 | It was first thinking of tracking down the error and sending a pull request to fix it. However, at the time I needed a 64 | quick solution and I had no idea where to start tracing the error. So I made a fork of the project, but basically 65 | rewrote chunks of the code (copy/pasting) a lot of it as well. 66 | 67 | 68 | How can I help? 69 | --------------- 70 | 71 | I am planning to maintain this tool in the future as to be able to support future changes to the OpenVAS report format. 72 | I also plan on adding more functionality as I feel the need for it, or receive requests from others. 73 | 74 | If you feel like I'd need to implement a new feature, rewrite some code, fix a bug, ... 75 | hit me up on `Twitter `_ 76 | or file an issue on `GitHub `_. 77 | 78 | TODO list 79 | --------- 80 | 81 | .. todolist:: 82 | 83 | 84 | .. |codecov| image:: https://codecov.io/gh/TheGroundZero/openvasreporting/branch/master/graph/badge.svg 85 | :alt: Code Coverage 86 | :target: https://codecov.io/gh/TheGroundZero/openvasreporting 87 | .. |docs| image:: https://readthedocs.org/projects/openvas-reporting/badge/?version=latest&style=flat 88 | :target: https://openvas-reporting.sequr.be/en/latest/ 89 | :alt: Documentation 90 | .. |license| image:: https://img.shields.io/github/license/TheGroundZero/openvasreporting.svg 91 | :alt: GitHub 92 | :target: https://github.com/TheGroundZero/openvasreporting/blob/master/LICENSE 93 | .. |pypi_format| image:: https://img.shields.io/pypi/format/OpenVAS-Reporting.svg 94 | :alt: PyPI - Format 95 | :target: https://pypi.org/project/OpenVAS-Reporting/ 96 | .. |pypi_version| image:: https://img.shields.io/pypi/v/OpenVAS-Reporting.svg 97 | :alt: PyPI - Version 98 | :target: https://pypi.org/project/OpenVAS-Reporting/ 99 | .. |version| image:: https://badge.fury.io/gh/TheGroundZero%2Fopenvasreporting.svg 100 | :target: https://badge.fury.io/gh/TheGroundZero%2Fopenvasreporting 101 | .. |vulns| image:: https://snyk.io/test/github/TheGroundZero/openvasreporting/badge.svg?targetFile=requirements.txt 102 | :alt: Known Vulnerabilities 103 | :target: https://snyk.io/test/github/TheGroundZero/openvasreporting?targetFile=requirements.txt 104 | -------------------------------------------------------------------------------- /openvasreporting/openvasreporting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 4 | # Project name: OpenVAS Reporting: A tool to convert OpenVAS XML reports into Excel files. 5 | # Project URL: https://github.com/TheGroundZero/openvasreporting 6 | 7 | import argparse 8 | 9 | from .libs.config import Config, Config_YAML 10 | from .libs.parser import parsers 11 | from .libs.export import implemented_exporters 12 | 13 | def main(): 14 | 15 | PROG_DESCRIPTION='''OpenVAS report Converter\n 16 | Parses one ore more OpenVAS xml report and creates a xlsx, docx or csv with the results 17 | ''' 18 | CONFIG_FILE_HELP="""path to a .yml file containing all options but INPUT_FILES and OUTPUT_FILE. 19 | if present, all the following options will be ignored and defaults applied 20 | when not present in this file. a sample of this file can be found in the doc folder\n""" 21 | REGEX_INCLUDE_HELP="""Path to a file containing a list of regex expressions to include in the report 22 | the regex expressions will be matched against the name of the vulnerability\n""" 23 | REGEX_EXCLUDE_HELP="""Path to a file containing a list of regex expressions to exclude from the report 24 | the regex expressions will be matched against the name of the vulnerability\n""" 25 | 26 | 27 | parser = argparse.ArgumentParser( 28 | prog="openvasreporting", # TODO figure out why I need this in my code for -h to show correct name 29 | description=PROG_DESCRIPTION, 30 | allow_abbrev=True, 31 | formatter_class=argparse.ArgumentDefaultsHelpFormatter 32 | ) 33 | parser.add_argument("-i", "--input", nargs="*", dest="input_files", help="OpenVAS XML reports\n", 34 | required=True) 35 | parser.add_argument("-o", "--output", dest="output_file", help="Output file, no extension\n", 36 | required=False, default="openvas_report") 37 | parser.add_argument("-c", "--config-file", dest="config_file",help=CONFIG_FILE_HELP, 38 | required=False, default=None) 39 | parser.add_argument("-l", "--level", dest="min_lvl", 40 | help="Minimal level (c, h, m, l, n)\n", 41 | required=False, choices=['c', 'h', 'm', 'l', 'n'], default='n') 42 | parser.add_argument("-f", "--format", dest="format", help="Output format (xlsx)\n", 43 | required=False, choices=["xlsx", "docx", "csv"], default="xlsx") 44 | parser.add_argument("-t", "--template", dest="template", 45 | help="Template file for docx export\n", required=False, 46 | default=None) 47 | parser.add_argument("-T", "--report-type", dest="report_type", help="Report by (v)ulnerability or by (h)ost\n", 48 | required=False, choices=['h', 'host', 'v', 'vulnerability', 's', 'summary'], default="vulnerability") 49 | parser.add_argument("-n", "--network-include", dest="networks_included", 50 | help="Path to a file containing a list of network cidrs, ip ranges and ips to be included in the report\n", 51 | required=False, default=None) 52 | parser.add_argument("-N", "--network-exclude", dest="networks_excluded", 53 | help="Path to a file containing a list of network cidrs, ip ranges and ips to be excluded from the report\n", 54 | required=False, default=None) 55 | parser.add_argument("-r", "--regex_include", dest="regex_included", help=REGEX_INCLUDE_HELP, 56 | required=False, default=None) 57 | parser.add_argument("-R", "--regex_exclude", dest="regex_excluded", help=REGEX_EXCLUDE_HELP, 58 | required=False, default=None) 59 | parser.add_argument("-e", "--cve-include", dest="cve_included", 60 | help="Path to a file containing a list of cve numbers to include in the report\n", 61 | required=False, default=None) 62 | parser.add_argument("-E", "--cve-exclude", dest="cve_excluded", 63 | help="Path to a file containing a list of cve numbers to exclude from the report\n", 64 | required=False, default=None) 65 | args = parser.parse_args() 66 | 67 | if not args.config_file is None: 68 | config = Config_YAML(args.input_files, args.config_file, args.output_file) 69 | else: 70 | config = Config(args.input_files, 71 | args.output_file, 72 | args.min_lvl, 73 | args.format, 74 | args.report_type, 75 | args.template, 76 | args.networks_included, 77 | args.networks_excluded, 78 | args.regex_included, 79 | args.regex_excluded, 80 | args.cve_included, 81 | args.cve_excluded) 82 | 83 | convert(config) 84 | 85 | 86 | def convert(config): 87 | """ 88 | Convert the OpenVAS XML to requested format 89 | 90 | :param config: configuration 91 | :type config: Config 92 | 93 | :raises: TypeError, ValueError, IOError, NotImplementedError 94 | """ 95 | if not isinstance(config, Config): 96 | raise TypeError("Expected Config, got '{}' instead".format(type(config))) 97 | 98 | if config.report_type + '-' + config.format not in implemented_exporters().keys(): 99 | raise NotImplementedError("The report by '{}' in format '{}' is not implemented yet.".format( 100 | config.report_type, config.format)) 101 | 102 | openvas_info = parsers()[config.report_type](config) 103 | 104 | implemented_exporters()[config.report_type + '-' + config.format](openvas_info, config.template, config.output_file) 105 | 106 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.abspath('..')) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'OpenVAS Reporting' 24 | copyright = '2018, TheGroundZero (@DezeStijn)' 25 | author = 'TheGroundZero (@DezeStijn)' 26 | 27 | # The short X.Y version 28 | version = '1.6' 29 | # The full version, including alpha/beta/rc tags 30 | release = '1.6.1' 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.todo' 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # 52 | # source_suffix = ['.rst', '.md'] 53 | source_suffix = '.rst' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | # 61 | # This is also used if you do content translation via gettext catalogs. 62 | # Usually you set "language" from the command line for these cases. 63 | language = None 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | # This pattern also affects html_static_path and html_extra_path . 68 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 69 | 70 | # The name of the Pygments (syntax highlighting) style to use. 71 | pygments_style = 'sphinx' 72 | 73 | 74 | # -- Options for HTML output ------------------------------------------------- 75 | 76 | # The theme to use for HTML and HTML Help pages. See the documentation for 77 | # a list of builtin themes. 78 | # 79 | html_theme = 'alabaster' 80 | 81 | # Theme options are theme-specific and customize the look and feel of a theme 82 | # further. For a list of options available for each theme, see the 83 | # documentation. 84 | # 85 | # html_theme_options = {} 86 | html_theme_options = { 87 | 'logo': 'img/OpenVASreporting.png', 88 | 'touch_icon': 'img/OpenVASreporting.png', 89 | 'logo_name': False, 90 | 'github_user': 'TheGroundZero', 91 | 'github_repo': 'openvasreporting', 92 | 'github_button': True, 93 | 'sidebar_collapse': False, 94 | 'show_relbars': True, 95 | 'fixed_sidebar': True, 96 | } 97 | # See: https://alabaster.readthedocs.io/en/latest/customization.html#variables-and-feature-toggles 98 | 99 | # Add any paths that contain custom static files (such as style sheets) here, 100 | # relative to this directory. They are copied after the builtin static files, 101 | # so a file named "default.css" will overwrite the builtin "default.css". 102 | html_static_path = ['_static'] 103 | 104 | # Custom sidebar templates, must be a dictionary that maps document names 105 | # to template names. 106 | # 107 | # The default sidebars (for documents that don't match any pattern) are 108 | # defined by theme itself. Builtin themes are using these templates by 109 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 110 | # 'searchbox.html']``. 111 | # 112 | # html_sidebars = {} 113 | 114 | # -- Options for HTMLHelp output --------------------------------------------- 115 | 116 | # Output file base name for HTML help builder. 117 | htmlhelp_basename = 'OpenVASReportingdoc' 118 | 119 | 120 | # -- Options for LaTeX output ------------------------------------------------ 121 | 122 | latex_elements = { 123 | # The paper size ('letterpaper' or 'a4paper'). 124 | # 125 | # 'papersize': 'letterpaper', 126 | 'papersize': 'a4paper', 127 | 128 | # The font size ('10pt', '11pt' or '12pt'). 129 | # 130 | # 'pointsize': '10pt', 131 | 132 | # Additional stuff for the LaTeX preamble. 133 | # 134 | # 'preamble': '', 135 | 136 | # Latex figure (float) alignment 137 | # 138 | # 'figure_align': 'htbp', 139 | } 140 | 141 | # Grouping the document tree into LaTeX files. List of tuples 142 | # (source start file, target name, title, 143 | # author, documentclass [howto, manual, or own class]). 144 | latex_documents = [ 145 | (master_doc, 'OpenVASReporting.tex', 'OpenVAS Reporting Documentation', 146 | 'TheGroundZero', 'manual'), 147 | ] 148 | 149 | 150 | # -- Options for manual page output ------------------------------------------ 151 | 152 | # One entry per manual page. List of tuples 153 | # (source start file, name, description, authors, manual section). 154 | man_pages = [ 155 | (master_doc, 'openvasreporting', 'OpenVAS Reporting Documentation', 156 | [author], 1) 157 | ] 158 | 159 | 160 | # -- Options for Texinfo output ---------------------------------------------- 161 | 162 | # Grouping the document tree into Texinfo files. List of tuples 163 | # (source start file, target name, title, author, 164 | # dir menu entry, description, category) 165 | texinfo_documents = [ 166 | (master_doc, 'OpenVASReporting', 'OpenVAS Reporting Documentation', 167 | author, 'OpenVASReporting', 'A tool to convert OpenVAS/Greenbone XML reports into Excel files..', 168 | 'Miscellaneous'), 169 | ] 170 | 171 | 172 | # -- Extension configuration ------------------------------------------------- 173 | 174 | # -- Options for todo extension ---------------------------------------------- 175 | 176 | # If true, `todo` and `todoList` produce output, else they produce nothing. 177 | todo_include_todos = True 178 | todo_link_only = True 179 | 180 | # -- Custom configuration ---------------------------------------------------- 181 | 182 | smartquotes = False 183 | -------------------------------------------------------------------------------- /openvasreporting/libs/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 4 | # Project name: OpenVAS Reporting: A tool to convert OpenVAS XML reports into Excel files. 5 | # Project URL: https://github.com/TheGroundZero/openvasreporting 6 | 7 | # TODO: get rid of the log clutter 8 | 9 | import logging 10 | from .config import Config 11 | from .parsed_data import ResultTree, Host, Port, Vulnerability, ParseVulnerability 12 | 13 | # 14 | # DEBUG 15 | 16 | #import sys 17 | #import logging 18 | #logging.basicConfig(stream=sys.stderr, level=logging.DEBUG, 19 | # format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S") 20 | # logging.basicConfig(stream=sys.stderr, level=logging.ERROR, 21 | # format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S") 22 | dolog = False 23 | 24 | #__all__ = ["openvas_parser"] 25 | 26 | from defusedxml import ElementTree as Et 27 | 28 | def parsers(): 29 | """ 30 | Enum-like instance containing references to correct parser function 31 | 32 | > parsers()[key](param[s]) 33 | 34 | :return: Pointer to parser function 35 | """ 36 | return { 37 | 'vulnerability': openvas_parser_by_vuln, 38 | 'host': openvas_parser_by_host, 39 | 'summary': openvas_parser_by_vuln 40 | } 41 | 42 | def openvas_parser_by_vuln(config: Config): 43 | """ 44 | This function takes an OpenVAS XML report and returns Vulnerability info 45 | 46 | :param config: config options as by the command line or defaults 47 | :type config: Config 48 | 49 | :return: list 50 | 51 | :raises: TypeError, InvalidFormat 52 | """ 53 | 54 | if not isinstance(config, Config): 55 | raise TypeError("Expected Config, got '{}' instead".format(type(config))) 56 | for file in config.input_files: 57 | if not isinstance(file, str): 58 | raise TypeError( 59 | "Expected basestring, got '{}' instead".format(type(file))) 60 | with open(file, "r", newline=None) as f: 61 | first_line = f.readline() 62 | if not first_line.startswith(" found. Check exclusions and/or scope listings. I\'ve got nothing to do') 158 | 159 | return resulttree 160 | 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenVAS Reporting: 2 | 3 | [![GitHub version](https://badge.fury.io/gh/TheGroundZero%2Fopenvasreporting.svg)](https://badge.fury.io/gh/TheGroundZero%2Fopenvasreporting) 4 | [![License](https://img.shields.io/github/license/TheGroundZero/openvasreporting.svg)](https://github.com/TheGroundZero/openvasreporting/blob/master/LICENSE) 5 | [![Docs](https://readthedocs.org/projects/openvas-reporting/badge/?version=latest&style=flat)](https://openvas-reporting.sequr.be) 6 | [![PyPI - Version](https://img.shields.io/pypi/v/OpenVAS-Reporting.svg)](https://pypi.org/project/OpenVAS-Reporting/) 7 | [![PyPI - Format](https://img.shields.io/pypi/format/OpenVAS-Reporting.svg)](https://pypi.org/project/OpenVAS-Reporting/) 8 | [![Known Vulnerabilities](https://snyk.io/test/github/TheGroundZero/openvasreporting/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/TheGroundZero/openvasreporting?targetFile=requirements.txt) 9 | [![codecov](https://codecov.io/gh/TheGroundZero/openvasreporting/branch/master/graph/badge.svg)](https://codecov.io/gh/TheGroundZero/openvasreporting) 10 | 11 | A tool to convert [OpenVAS](http://www.openvas.org/) XML into reports. 12 | 13 | ![Report example screenshot](docs/_static/img/OpenVASreporting.png?raw=true) 14 | 15 | *Read the full documentation at [https://openvas-reporting.sequr.be](https://openvas-reporting.sequr.be)* 16 | 17 | # LOOKING FOR MAINTAINERS 18 | 19 | **THIS PROJECT IS NO LONGER ACTIVELY MAINTAINED!** 20 | 21 | **PULL REQUESTS FOR MINOR CHANGES MAY STILL BE ACCEPTED. 22 | CHANGES IN OPENVAS MAY (and likely will) BREAK THIS TOOL. I WILL NOT PROVIDE SUPPORT FOR THAT.** 23 | 24 | --- 25 | 26 | I forked [OpenVAS2Report](https://github.com/cr0hn/openvas_to_report) since it didn't manage to convert all reports I threw at it 27 | and because I wanted to learn how to use Python for working with XML and creating Excel files. 28 | Also, OpenVAS mixes their own threat levels with the [CVSS](https://www.first.org/cvss/) scoring, the latter of which I prefer to use in my reports. 29 | 30 | Looking for a fix and providing an actual fix through a pull request would have been too much work, 31 | so I chose to fork the repo and try my own thing. 32 | I reorganised some of the files, removed some functionality and added some extra, and rewrote some functions. 33 | 34 | At this moment in time, the script only output .xlsx documents in one format, this may (not) change in the future. 35 | 36 | 37 | ## Requirements 38 | 39 | - [Python](https://www.python.org/) version 3 40 | - [XlsxWriter](https://xlsxwriter.readthedocs.io/) 41 | - [Python-docx](https://python-docx.readthedocs.io) 42 | 43 | 44 | ## Installation 45 | 46 | # Install Python3 and pip3 47 | apt(-get) install python3 python3-pip # Debian, Ubuntu 48 | yum -y install python3 python3-pip # CentOS 49 | dnf install python3 python3-pip # Fedora 50 | # Clone repo 51 | git clone https://github.com/TheGroundZero/openvasreporting.git 52 | # Install required python packages 53 | cd openvasreporting 54 | pip3 install pip --upgrade 55 | pip3 install build --upgrade 56 | python -m build 57 | # Install module 58 | pip3 install dist/OpenVAS_Reporting-X.x.x-py3-xxxx-xxx.whl 59 | 60 | 61 | Alternatively, you can install the package through the Python package installer 'pip'. 62 | This currently has some issues (see #4) 63 | 64 | # Install Python3 and pip3 65 | apt(-get) install python3 python3-pip # Debian, Ubuntu 66 | yum -y install python3 python3-pip # CentOS 67 | dnf install python3 python3-pip # Fedora 68 | # Install the package 69 | pip3 install OpenVAS-Reporting 70 | 71 | 72 | ## Usage 73 | 74 | # When working from the Git repo 75 | python3 -m openvasreporting -i [OpenVAS xml file(s)] [-o [Output file]] [-f [Output format]] [-l [minimal threat level (n, l, m, h, c)]] [-t [docx template]] 76 | # When using the pip package 77 | openvasreporting -i [OpenVAS xml file(s)] [-o [Output file]] [-f [Output format]] [-l [minimal threat level (n, l, m, h, c)]] [-t [docx template]] 78 | 79 | ### Parameters 80 | 81 | | Short param | Long param | Description | Required | Default value | 82 | | :---------: | :---------------: | :------------------: | :------: | :----------------------------------------- | 83 | | -i | --input | Input file(s) | YES | n/a | 84 | | -o | --output | Output filename | No | openvas\_report | 85 | | -c | --config-file | .yml configuration | No | None | 86 | | -f | --format | Output format | No | xlsx | 87 | | -l | --level | Minimal level | No | n | 88 | | -T | --report-type | Report by | No | vulnerability | 89 | | | | vulnerability | | | 90 | | | | or by host | | | 91 | | -t | --template | Docx template | No | openvasreporting/src/openvas-template.docx | 92 | | -n | --network-include | file with networks | No | None | 93 | | | | to include | | | 94 | | -N | --network-exclude | file with networks | No | None | 95 | | | | to exclude | | | 96 | | -r | --regex-include | file with regex to | No | None | 97 | | | | to include from name | | | 98 | | -R | --regex-exclude | file with regex to | No | None | 99 | | | | to exclude from name | | | 100 | | -e | --cve-include | file with CVEs to | No | None | 101 | | | | to include from name | | | 102 | | -E | --cve-exclude | file with CVEs to | No | None | 103 | | | | to exclude from name | | | 104 | 105 | ## Filtering options 106 | 107 | The `-n`/`-N`/`-r`/`-R`/`-e`/`-E` options will read a file with one option per line. 108 | Networks accepts CIDRs, IP Ranges or IPs. 109 | Regex accept any valid regex expression and will be case insensitive matched against the name of the vulnerability. 110 | CVEs are inserted in the `CVE-YYYY-nnnnn` format. 111 | 112 | The `-c` option will read a .yml file with all configurations. 113 | If the `-c` option is used, any other options but input and output filenames are ignored. 114 | There is a sample of a configuration file in the `docs/` folder 115 | 116 | ## Examples 117 | 118 | ### Create Excel report from 1 OpenVAS XML report using default settings 119 | 120 | python3 -m openvasreporting -i openvasreport.xml -f xlsx 121 | 122 | ### Create Excel report from multiple OpenVAS reports using default settings 123 | 124 | # wildcard select 125 | python3 -m openvasreporting -i *.xml -f xlsx 126 | # selective 127 | python3 -m openvasreporting -i openvasreport1.xml -i openvasreport2.xml -f xlsx 128 | 129 | ### Create Word report from multiple OpenVAS reports, reporting only threat level high and up, use custom template 130 | 131 | python3 -m openvasreporting -i *.xml -o docxreport -f docx -l h -t "/home/user/myOpenvasTemplate.docx" 132 | 133 | ## Result 134 | 135 | The final report (in Excel format) will then look something like this: 136 | 137 | ![Report example screenshot - Summary](docs/_static/img/screenshot-report.png?raw=true) 138 | ![Report example screenshot - ToC](docs/_static/img/screenshot-report1.png?raw=true) 139 | ![Report example screenshot - Vuln desc](docs/_static/img/screenshot-report2.png?raw=true) 140 | 141 | Worksheets are sorted according to CVSS score and are colored according to the vulnerability level. 142 | 143 | ## Ideas 144 | 145 | Some of the ideas I still have for future functionality: 146 | 147 | - list vulnerabilities per host ==DONE== 148 | - filter by host (scope/exclude) as in OpenVAS2Report ==DONE== 149 | - select threat levels individually (e.g. none and low; but not med, high and crit) 150 | - import other formats (not only XML), e.g. CSV as suggested in [this issue](https://github.com/TheGroundZero/openvasreporting_server/issues/3) 151 | -------------------------------------------------------------------------------- /openvasreporting/libs/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 4 | # Project name: OpenVAS Reporting: A tool to convert OpenVAS XML reports into Excel files. 5 | # Project URL: https://github.com/TheGroundZero/openvasreporting 6 | 7 | """This file contains data structures""" 8 | 9 | import re 10 | import netaddr 11 | import glob 12 | 13 | import yaml 14 | from yaml.loader import SafeLoader 15 | 16 | class Config(object): 17 | def __init__(self, input_files, output_file="openvas_report", min_level="n", format="xlsx", 18 | report_type="host", template=None, networks_included=None, networks_excluded=None, 19 | regex_included=None, regex_excluded=None, cve_included=None, cve_excluded=None): 20 | """ 21 | :param input_files: input file path 22 | :type input_files: list(str) 23 | 24 | :param output_file: output file path and name 25 | :type output_file: str 26 | 27 | :param min_level: minimal level to add to output 28 | :type min_level: str 29 | 30 | :param format: output file format 31 | :type format: str 32 | 33 | :param report_type: report by (v)ulnerability or (h)ost 34 | :type report_type: str 35 | 36 | :param template: template to use 37 | :type format: str 38 | 39 | :param networks_excluded: path to file with a list of excluded host ips 40 | :type networks_excluded: str 41 | 42 | :param networks_included: path to file with a list of included host ips 43 | :type networks_included: str 44 | 45 | :param regex_excluded: path to file with a list of regex expressions 46 | to be excluded when matched against a vulnerability description 47 | 48 | :param regex_included: path to file with a list of regex expressions 49 | to be included when matched against a vulnerability description 50 | 51 | :raises: TypeError, ValueError 52 | """ 53 | if not isinstance(input_files, list): 54 | raise TypeError("Expected list, got '{}' instead".format(type(input_files))) 55 | else: 56 | for i in input_files: 57 | if not isinstance(i, str): 58 | raise TypeError("Expected str, got '{}' instead".format(type(i))) 59 | 60 | if not isinstance(output_file, str): 61 | raise TypeError("Expected str, got '{}' instead".format(type(output_file))) 62 | if not isinstance(min_level, str): 63 | raise TypeError("Expected str, got '{}' instead".format(type(min_level))) 64 | if not isinstance(format, str): 65 | raise TypeError("Expected str, got '{}' instead".format(type(format))) 66 | if template is not None and not isinstance(template, str): 67 | raise TypeError("Expected str, got '{}' instead".format(type(template))) 68 | if report_type is not None and not isinstance(report_type, str): 69 | raise TypeError("Expected str, got '{}' instead".format(type(report_type))) 70 | if networks_excluded is not None and not isinstance(networks_excluded, str): 71 | raise TypeError("Expected str, got '{}' instead".format(type(networks_excluded))) 72 | if networks_included is not None and not isinstance(networks_included, str): 73 | raise TypeError("Expected str, got '{}' instead".format(type(networks_included))) 74 | if regex_excluded is not None and not isinstance(regex_excluded, str): 75 | raise TypeError("Expected str, got '{}' instead".format(type(regex_excluded))) 76 | if regex_included is not None and not isinstance(regex_included, str): 77 | raise TypeError("Expected str, got '{}' instead".format(type(regex_included))) 78 | if cve_excluded is not None and not isinstance(cve_excluded, str): 79 | raise TypeError("Expected str, got '{}' instead".format(type(cve_excluded))) 80 | if cve_included is not None and not isinstance(cve_included, str): 81 | raise TypeError("Expected str, got '{}' instead".format(type(cve_included))) 82 | 83 | if min_level.lower() in Config.levels().keys(): 84 | min_level = Config.levels()[min_level.lower()] 85 | else: 86 | raise ValueError("Invalid value for level parameter, \ 87 | must be one of: c[ritical], h[igh], m[edium], l[low], n[one]") 88 | 89 | ifiles = [] 90 | for file in input_files: 91 | ifiles.extend(glob.glob(file)) 92 | self.input_files = ifiles 93 | self.output_file = "{}.{}".format(output_file, format) if output_file.split(".")[-1] != format \ 94 | else output_file 95 | 96 | self.min_level = min_level 97 | self.format = format 98 | self.template = template 99 | 100 | self.report_type = report_type 101 | if report_type == 'v' or report_type == 'vulnerability': 102 | self.report_type = 'vulnerability' 103 | elif report_type == 'h' or report_type == 'host': 104 | self.report_type = 'host' 105 | elif report_type == 's' or report_type == 'summary': 106 | self.report_type = 'summary' 107 | else: 108 | raise ValueError("Expected host or vulnerability for report-type parameter but got '{}' instead.".format(report_type)) 109 | 110 | if not networks_excluded is None: 111 | with open(networks_excluded) as f: 112 | lines = f.read().splitlines() 113 | self.networks_excluded = self.include_networks(lines) 114 | else: 115 | self.networks_excluded = None 116 | 117 | if not networks_included is None: 118 | with open(networks_included) as f: 119 | lines = f.read().splitlines() 120 | self.networks_included = self.include_networks(lines) 121 | else: 122 | self.networks_included = None 123 | 124 | if not regex_excluded is None: 125 | with open(regex_excluded) as f: 126 | lines = f.read().splitlines() 127 | self.regex_excluded = self.include_regex(lines) 128 | else: 129 | self.regex_excluded = None 130 | 131 | if not regex_included is None: 132 | with open(regex_included) as f: 133 | lines = f.read().splitlines() 134 | self.regex_included = self.include_regex(lines) 135 | else: 136 | self.regex_included = None 137 | 138 | if not cve_included is None: 139 | with open(cve_included) as f: 140 | self.cve_included = f.read().splitlines() 141 | else: 142 | self.cve_included = None 143 | 144 | if not cve_excluded is None: 145 | with open(cve_excluded) as f: 146 | self.cve_excluded = f.read().splitlines() 147 | else: 148 | self.cve_excluded = None 149 | 150 | @staticmethod 151 | def colors(): 152 | return { 153 | 'blue': '#183868', 154 | 'critical': '#702da0', 155 | 'high': '#c80000', 156 | 'medium': '#ffc000', 157 | 'low': '#00b050', 158 | 'none': '#0070c0', 159 | } 160 | 161 | 162 | @staticmethod 163 | def levels(): 164 | return { 165 | 'c': 'critical', 166 | 'h': 'high', 167 | 'm': 'medium', 168 | 'l': 'low', 169 | 'n': 'none' 170 | } 171 | 172 | @staticmethod 173 | def thresholds(): 174 | return { 175 | 'critical': 9.0, 176 | 'high': 7.0, 177 | 'medium': 4.0, 178 | 'low': 0.1, 179 | 'none': 0.0 180 | } 181 | 182 | @staticmethod 183 | def cvss_color(cvss): 184 | for key in Config.thresholds(): 185 | if cvss >= Config.thresholds()[key]: 186 | return Config.colors()[key] 187 | return None 188 | 189 | @staticmethod 190 | def cvss_level(cvss): 191 | for key in Config.thresholds(): 192 | if cvss >= Config.thresholds()[key]: 193 | return key 194 | return None 195 | 196 | @staticmethod 197 | def min_levels(): 198 | return { 199 | 'critical': [Config.levels()['c']], 200 | 'high': [Config.levels()['c'], Config.levels()['h']], 201 | 'medium': [Config.levels()['c'], Config.levels()['h'], Config.levels()['m']], 202 | 'low': [Config.levels()['c'], Config.levels()['h'], Config.levels()['m'], Config.levels()['l']], 203 | 'none': [Config.levels()['c'], Config.levels()['h'], Config.levels()['m'], Config.levels()['l'], 204 | Config.levels()['n']] 205 | } 206 | 207 | # 208 | # includes lines from options files networks-includes and networks-excludes 209 | # into a list of netaddr instances for later comparision when parsing and filtering 210 | def include_networks(self, lines): 211 | outlines = [] 212 | for ip in lines: 213 | if ip == '': 214 | continue 215 | if '-' in ip: # ip range? 216 | _start_ip, _end_ip = ip.split('-') 217 | try: 218 | ip_range = netaddr.IPRange(_start_ip, _end_ip) 219 | outlines.append(ip_range) 220 | except AddrFormatError: 221 | raise AddrFormatError("Expected valid ip range, got '{}'-'{}' instead".format(_start_ip, _end_ip)) 222 | else: # ip or network cdir? 223 | try: 224 | network = netaddr.IPNetwork(ip) 225 | outlines.append(network) 226 | except AddrFormatError: 227 | raise AddrFormatError("Expected valid ip address or network cidr, got '{}' instead.".format(ip)) 228 | 229 | return outlines 230 | 231 | # 232 | # includes lines from options files regex-includes and regex-excludes 233 | # into a list of re.compile(d) instances for later comparision when parsing and filtering 234 | def include_regex(self, lines): 235 | outlines = [] 236 | for regex_entry in lines: 237 | try: 238 | outlines.append(re.compile(regex_entry, re.IGNORECASE)) 239 | except re.error: 240 | raise ValueError("Expected valid regex expression, got '{}' instead.".format(regex_entry)) 241 | 242 | return outlines 243 | 244 | class Config_YAML(Config): 245 | def __init__(self, input_files, config_file, output_file="openvas_report"): 246 | """ 247 | :param input_files: input file path 248 | :type input_files: list(str) 249 | 250 | :param output_file: output file path and name 251 | :type output_file: str 252 | 253 | :param min_level: minimal level to add to output 254 | :type min_level: str 255 | 256 | :raises: TypeError, ValueError 257 | """ 258 | 259 | # call parent class 260 | Config.__init__(self, input_files) 261 | 262 | if not isinstance(input_files, list): 263 | raise TypeError("Expected list, got '{}' instead".format(type(input_files))) 264 | else: 265 | for i in input_files: 266 | if not isinstance(i, str): 267 | raise TypeError("Expected str, got '{}' instead".format(type(i))) 268 | 269 | if not isinstance(output_file, str): 270 | raise TypeError("Expected str, got '{}' instead".format(type(output_file))) 271 | if not isinstance(config_file, str): 272 | raise TypeError("Expected str, got '{}' instead".format(type(min_level))) 273 | 274 | ifiles = [] 275 | for file in input_files: 276 | ifiles.extend(glob.glob(file)) 277 | self.input_files = ifiles 278 | 279 | # loads configuration .yml file as a dict 280 | try: 281 | with open(config_file, 'r') as f: 282 | yamldict = yaml.load(f, Loader=SafeLoader) 283 | except FileNotFoundError: 284 | raise FileNotFoundError("Could Not find '{}'.".format(config_file)); 285 | 286 | # set options flags from yml or defaults 287 | if 'level' in yamldict: 288 | if not yamldict['level'] in ['critical', 'high', 'medium', 'low', 'none']: 289 | raise ValueError("Invalid value for level parameter, \ 290 | must be one of: critical, high, medium, low, none") 291 | self.min_level = yamldict['level'] 292 | else: 293 | self.min_level = 'none'; 294 | 295 | if 'template' in yamldict: 296 | self.template = yamldict['template'] 297 | else: 298 | self.template = None 299 | 300 | if 'format' in yamldict: 301 | if not yamldict['format'] in ['xlsx', 'docx', 'csv']: 302 | raise ValueError("Invalid value for format parameter, must be one of: xlsx, docx, csv") 303 | self.format = yamldict['format'] 304 | else: 305 | self.format = 'xlsx' 306 | 307 | if 'reporttype' in yamldict: 308 | if not yamldict['reporttype'] in ['vulnerability', 'host', 'summary']: 309 | raise ValueError("Invalid value for report type parameter, must be one of: vulnerability, host, summary") 310 | self.report_type = yamldict['reporttype'] 311 | else: 312 | self.report_type = 'vulnerability' 313 | 314 | # network filters 315 | if 'networks' in yamldict: 316 | if 'includes' in yamldict['networks']: 317 | self.networks_included = self.include_networks(yamldict['networks']['includes']) 318 | else: 319 | self.networks_included = None 320 | if 'excludes' in yamldict['networks']: 321 | self.networks_excluded = self.include_networks(yamldict['networks']['excludes']) 322 | else: 323 | self.networks_excluded = None 324 | else: 325 | self.networks_excluded = None 326 | self.networks_included = None 327 | 328 | # regex filters 329 | if 'regex' in yamldict: 330 | if 'includes' in yamldict['regex']: 331 | self.regex_included = self.include_regex(yamldict['regex']['includes']) 332 | else: 333 | self.regex_included = None 334 | if 'excludes' in yamldict['regex']: 335 | self.regex_excluded = self.include_regex(yamldict['regex']['excludes']) 336 | else: 337 | self.regex_included = None 338 | else: 339 | self.regex_excluded = None 340 | self.regex_included = None 341 | 342 | # cve filters 343 | if 'cve' in yamldict: 344 | if 'includes' in yamldict['cve']: 345 | self.cve_included = yamldict['cve']['includes'] 346 | else: 347 | self.cve_included = None 348 | if 'excludes' in yamldict['cve']: 349 | self.cve_excluded = yamldict['cve']['excludes'] 350 | else: 351 | self.cve_excluded = None 352 | else: 353 | self.cve_excluded = None 354 | self.cve_included = None 355 | 356 | self.output_file = "{}.{}".format(output_file, self.format) if output_file.split(".")[-1] != self.format \ 357 | else output_file 358 | 359 | 360 | -------------------------------------------------------------------------------- /openvasreporting/libs/parsed_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 4 | # Project name: OpenVAS Reporting: A tool to convert OpenVAS XML reports into Excel files. 5 | # Project URL: https://github.com/TheGroundZero/openvasreporting 6 | 7 | # TODO: Get rid of all the log messages 8 | 9 | """This file contains data structures""" 10 | 11 | import re 12 | from typing import Union 13 | 14 | from .config import Config 15 | import netaddr 16 | 17 | import logging 18 | # 19 | # DEBUG 20 | 21 | #import logging 22 | #import sys 23 | #logging.basicConfig(stream=sys.stderr, level=logging.DEBUG, 24 | # format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S") 25 | #logging.basicConfig(stream=sys.stderr, level=logging.ERROR, 26 | # format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S") 27 | dolog = False 28 | 29 | # Port object modifed to include result data field 30 | class Port(object): 31 | """Port information""" 32 | 33 | def __init__(self, number:int, protocol:str="tcp", result:str=""): 34 | """ 35 | :param number: port number 36 | :type number: int 37 | 38 | :param protocol: port protocol (tcp, udp, ...) 39 | :type protocol: basestring 40 | 41 | :param result: port result 42 | :type result: str 43 | 44 | :raises: TypeError, ValueError 45 | """ 46 | if not isinstance(number, int): 47 | raise TypeError("Expected int, got '{}' instead".format(type(number))) 48 | else: 49 | if number < 0: 50 | raise ValueError("Port number must be greater than 0") 51 | 52 | if not isinstance(protocol, str): 53 | raise TypeError("Expected basestring, got '{}' instead".format(type(protocol))) 54 | 55 | if not isinstance(result, str): 56 | raise TypeError("Expected basestring, got '{}' instead".format(type(result))) 57 | 58 | self.number = number 59 | self.protocol = protocol 60 | self.result = result 61 | 62 | # Modified to include result in structure 63 | @staticmethod 64 | def string2port(info:str, result:str): 65 | """ 66 | Extract port number, protocol and description from an string. 67 | return a port class with seperate port, protocol and result 68 | 69 | ..note: 70 | Raises value error if information can't be processed. 71 | 72 | # >>> p=Port.string2port("2000/tcp","result string") 73 | # >>> print p.number 74 | 2000 75 | # >>> print p.proto 76 | "tcp" 77 | # >>> print p.result 78 | "result string" 79 | 80 | # >>> p=Port.string2port("general/icmp", "string test") 81 | # >>> print p.number 82 | 0 83 | # >>> print p.proto 84 | "icmp" 85 | # >>> print p.result 86 | "string test" 87 | 88 | :param info: raw string with port information 89 | :type info: basestring 90 | 91 | :param result: raw string with port information 92 | :type result: basestring 93 | 94 | :return: Port instance 95 | :rtype: Port 96 | 97 | :raises: ValueError 98 | """ 99 | if not isinstance(info, str): 100 | raise TypeError("Expected basestring, got '{}' instead".format(type(info))) 101 | 102 | if not isinstance(result, str): 103 | raise TypeError("Expected basestring, got '{}' instead".format(type(result))) 104 | 105 | regex_nr = re.search("([\d]+)(/)([\w]+)", info) # type: ignore 106 | regex_general = re.search("(general)(/)([\w]+)", info) # type: ignore 107 | 108 | if regex_nr and len(regex_nr.groups()) == 3: 109 | number = int(regex_nr.group(1)) 110 | protocol = regex_nr.group(3) 111 | elif regex_general and len(regex_general.groups()) == 3: 112 | number = 0 113 | protocol = regex_general.group(3) 114 | else: 115 | raise ValueError("Can't parse port input string") 116 | 117 | return Port(number, protocol, result) 118 | 119 | def __eq__(self, other:'Port'): 120 | return ( 121 | isinstance(other, Port) and 122 | other.number == self.number and 123 | other.protocol == self.protocol and 124 | other.result == self.result 125 | ) 126 | 127 | class ParseVulnerability: 128 | """ 129 | Parses and analyses a Vulnerability XML Entry 130 | """ 131 | def __init__(self, vuln, min_level: str): 132 | """ 133 | Parses an openvas xml Et.Element. 134 | 135 | : param: vuln: openvas xml report element 136 | : type: xml.etree import ElementTree as Et or xml.etree import cElementTree as Et 137 | 138 | : param: min_level: minimal level for inclusion on the report 139 | : type: one of {c, h, m, l, n} 140 | 141 | returns self instance populated with values from subtags 142 | """ 143 | if not isinstance(min_level, str): 144 | raise TypeError("expected str, got '{}' instead".format(type(str))) 145 | 146 | nvt_tmp = vuln.find("./nvt") 147 | 148 | # -------------------- 149 | # 150 | # VULN_NAME 151 | self.vuln_name:str = nvt_tmp.find("./name").text 152 | 153 | if dolog: logging.debug( 154 | "--------------------------------------------------------------------------------") 155 | if dolog: logging.debug("- {}".format(self.vuln_name)) # DEBUG 156 | if dolog: logging.debug( 157 | "--------------------------------------------------------------------------------") 158 | 159 | 160 | # -------------------- 161 | # 162 | # VULN_VERSION 163 | self.vuln_version = "" 164 | desc:str = vuln.find("./description").text 165 | if desc is None: 166 | desc = "" 167 | match = re.search(r'Installed version: ((\d|.)+)', desc) 168 | if match is not None : 169 | self.vuln_version = match.group(1) 170 | 171 | if dolog: logging.debug("* vuln_version:\t{}".format(self.vuln_version)) # DEBUG 172 | 173 | # -------------------- 174 | # 175 | # VULN_ID 176 | self.vuln_id = nvt_tmp.get("oid") 177 | if not self.vuln_id or self.vuln_id == "0": 178 | if dolog: logging.debug(" ==> SKIP") # DEBUG 179 | raise ValueError("Expected valid openvas xml element, got '{}' instead".format(vuln.text)) 180 | if dolog: logging.debug("* vuln_id:\t{}".format(self.vuln_id)) # DEBUG 181 | 182 | # -------------------- 183 | # 184 | # VULN_CVSS 185 | self.vuln_cvss = vuln.find("./severity").text 186 | if self.vuln_cvss is None: 187 | self.vuln_cvss = 0.0 188 | self.vuln_cvss = float(self.vuln_cvss) 189 | if dolog: logging.debug("* vuln_cvss:\t{}".format(self.vuln_cvss)) # DEBUG 190 | 191 | # -------------------- 192 | # 193 | # VULN_LEVEL 194 | self.vuln_level = "none" 195 | for level in Config.levels().values(): 196 | if self.vuln_cvss >= Config.thresholds()[level]: 197 | self.vuln_level = level 198 | if dolog: logging.debug("* vuln_level:\t{}".format(self.vuln_level)) # DEBUG 199 | break 200 | 201 | if dolog: logging.debug("* min_level:\t{}".format(min_level)) # DEBUG 202 | if self.vuln_level not in Config.min_levels()[min_level]: 203 | if dolog: logging.debug(" => SKIP") # DEBUG 204 | raise ValueError("Expected min_level in one of 'chmln', got '{}' instead".format(min_level)) 205 | 206 | # -------------------- 207 | # 208 | # VULN_HOST 209 | self.vuln_host:str = vuln.find("./host").text 210 | self.vuln_host_name:str = vuln.find("./host/hostname").text 211 | if self.vuln_host_name is None: 212 | self.vuln_host_name = "N/A" 213 | if dolog: logging.debug("* hostname:\t{}".format(self.vuln_host_name)) # DEBUG 214 | self.vuln_port = vuln.find("./port").text 215 | if dolog: logging.debug( 216 | "* vuln_host:\t{} port:\t{}".format(self.vuln_host, self.vuln_port)) # DEBUG 217 | 218 | # -------------------- 219 | # 220 | # VULN_TAGS 221 | # Replace double newlines by a single newline 222 | self.vuln_tags_text = re.sub(r"(\r\n)+", "\r\n", nvt_tmp.find("./tags").text) 223 | self.vuln_tags_text = re.sub(r"\n+", "\n", self.vuln_tags_text) 224 | # Remove useless whitespace but not newlines 225 | self.vuln_tags_text = re.sub(r"[^\S\r\n]+", " ", self.vuln_tags_text) 226 | vuln_tags_temp = self.vuln_tags_text.split('|') 227 | self.vuln_tags = dict(tag.split('=', 1) for tag in vuln_tags_temp) 228 | if dolog: logging.debug("* vuln_tags:\t{}".format(self.vuln_tags)) # DEBUG 229 | 230 | # -------------------- 231 | # 232 | # VULN_THREAT 233 | self.vuln_threat:str = vuln.find("./threat").text 234 | if self.vuln_threat is None: 235 | self.vuln_threat = Config.levels()["n"] 236 | else: 237 | self.vuln_threat = self.vuln_threat.lower() 238 | 239 | if dolog: logging.debug("* vuln_threat:\t{}".format(self.vuln_threat)) # DEBUG 240 | 241 | # -------------------- 242 | # 243 | # VULN_FAMILY 244 | self.vuln_family:str = nvt_tmp.find("./family").text 245 | 246 | if dolog: logging.debug("* vuln_family:\t{}".format(self.vuln_family)) # DEBUG 247 | 248 | # -------------------- 249 | # 250 | # VULN_CVES 251 | #vuln_cves = nvt_tmp.findall("./refs/ref") 252 | self.vuln_cves:list[str] = [] 253 | self.ref_list:list[str] = [] 254 | for reference in nvt_tmp.findall('./refs/ref'): 255 | if reference.attrib.get('type') == 'cve': 256 | self.vuln_cves.append(reference.attrib.get('id')) 257 | else: 258 | self.ref_list.append(reference.attrib.get('id')) 259 | # if dolog: logging.debug("* vuln_cves:\t{}".format(vuln_cves)) # DEBUG 260 | # if dolog: logging.debug("* vuln_cves:\t{}".format(Et.tostring(vuln_cves).decode())) # DEBUG 261 | # if vuln_cves is None or vuln_cves.text.lower() == "nocve": 262 | # vuln_cves = [] 263 | # else: 264 | # vuln_cves = [vuln_cves.text.lower()] 265 | self.vuln_references = ' , '.join(self.ref_list) 266 | if dolog: logging.debug("* vuln_cves:\t{}".format(self.vuln_cves)) # DEBUG 267 | if dolog: logging.debug("* vuln_references:\t{}".format(self.vuln_references)) 268 | # -------------------- 269 | # 270 | # VULN_REFERENCES 271 | # vuln_references = nvt_tmp.find("./xref") 272 | # if vuln_references is None or vuln_references.text.lower() == "noxref": 273 | # vuln_references = [] 274 | # else: 275 | # vuln_references = vuln_references.text.lower().replace("url:", "\n") 276 | 277 | # if dolog: logging.debug("* vuln_references:\t{}".format(vuln_references)) # DEBUG 278 | 279 | # -------------------- 280 | # 281 | # VULN_DESCRIPTION 282 | tmpVulnResult = vuln.find("./description") 283 | if tmpVulnResult is None or vuln.find("./description").text is None: 284 | self.vuln_result = "" 285 | else: 286 | self.vuln_result = tmpVulnResult.text 287 | 288 | # Replace double newlines by a single newline 289 | self.vuln_result:str = self.vuln_result.replace("(\r\n)+", "\n") 290 | 291 | if dolog: logging.debug("* vuln_result:\t{}".format(self.vuln_result)) # DEBUG 292 | 293 | @classmethod 294 | def check_and_parse_result(cls, vuln, config: Config): 295 | """ 296 | checks if this vulnerability result element in the openvas xml report 297 | will be included in the convertion. If so, it instantiates a ParsedVulnerability 298 | object that will parse the element and returns it. 299 | for now it checks: 300 | - if this has a valid nvt-oid 301 | - if this has a severity level equal or higher than min_lvl 302 | - check if host_name is in the list of excluded files and return None if so 303 | - check if host_name is in the list of included only files 304 | 305 | : param: vuln: openvas xml report element 306 | : type: xml.etree import ElementTree as Et or xml.etree import cElementTree as Et 307 | 308 | : param: min_level: minimal level for inclusion on the report 309 | : type: one of {c, h, m, l, n} 310 | """ 311 | if not isinstance(config, Config): 312 | raise TypeError("Expected Config, got '{}' instead".format(type(config))) 313 | 314 | 315 | # nvt has oid? 316 | vuln_id:str = vuln.find('./nvt').get('oid') 317 | if not vuln_id or vuln_id == "0": 318 | return None 319 | 320 | # is ip included and/or excluded? 321 | if config.networks_excluded is not None or config.networks_included is not None: 322 | host_ip:str = vuln.find('./host').text 323 | host_ip_addr = netaddr.IPAddress(host_ip) 324 | 325 | if config.networks_excluded is not None: 326 | for ipline in config.networks_excluded: 327 | if host_ip_addr in ipline: 328 | return None 329 | 330 | if config.networks_included is not None: 331 | _included = False 332 | for ipline in config.networks_included: 333 | if host_ip_addr in ipline: 334 | _included = True 335 | if not _included: 336 | return None 337 | 338 | # check regex expressions inclusion and exclusion 339 | if config.regex_excluded is not None or config.regex_included is not None: 340 | vuln_name:str = vuln.find('./name').text 341 | 342 | # does any of regex_excluded matches this vulnerability name? 343 | if config.regex_excluded is not None: 344 | for regex_entry in config.regex_excluded: 345 | if regex_entry.search(vuln_name): 346 | return None 347 | 348 | # does any of regex_included matches this vulnerability name? 349 | if config.regex_included is not None: 350 | _included = False 351 | for regex_entry in config.regex_included: 352 | if regex_entry.search(vuln_name): 353 | _included = True 354 | if not _included: 355 | return None 356 | 357 | # check cve include or exclude 358 | if config.cve_excluded is not None or config.cve_included is not None: 359 | cve_list:list[str] = [] 360 | for r in vuln.findall("./nvt/refs/ref[@type='cve']"): 361 | cve_list.append(r.attrib.get('id')) 362 | 363 | # does any config.cve_excluded is in this list? 364 | if config.cve_excluded is not None: 365 | for cve_entry in config.cve_excluded: 366 | if cve_entry in cve_list: 367 | return None 368 | 369 | # does any cve_include is in this list? 370 | if config.cve_included is not None: 371 | _included = False 372 | for cve_entry in config.cve_included: 373 | if cve_entry in cve_list: 374 | _included = True 375 | if not _included: 376 | return None 377 | 378 | # vuln severity >= min_level? 379 | vuln_cvss:float = vuln.find('./severity').text 380 | if vuln_cvss is None: 381 | vuln_cvss = 0.0 382 | vuln_cvss = float(vuln_cvss) 383 | if vuln_cvss >= Config.thresholds()[config.min_level]: 384 | return cls(vuln, config.min_level) 385 | 386 | return None 387 | 388 | class Host(object): 389 | """Host information""" 390 | 391 | def __init__(self, ip:str, host_name:str=""): 392 | """ 393 | :param ip: Host IP 394 | :type ip: basestring 395 | 396 | :param host_name: Host name 397 | :type host_name: basestring 398 | 399 | :raises: TypeError 400 | """ 401 | if not isinstance(ip, str): 402 | raise TypeError("Expected basestring, got '{}' instead".format(type(ip))) 403 | if not isinstance(host_name, str): 404 | raise TypeError("Expected basestring, got '{}' instead".format(type(host_name))) 405 | 406 | self.ip = ip 407 | self.host_name = host_name 408 | self.num_vulns = 0 409 | self.nv = {'critical': 0, 410 | 'high': 0, 411 | 'medium': 0, 412 | 'low': 0, 413 | 'none': 0 414 | } 415 | self.sum_cvss = 0 416 | self.higher_cvss = 0 417 | self.vuln_list = [] 418 | 419 | def addvulnerability(self, parsed_vuln: ParseVulnerability): 420 | """ 421 | Creates and adds a new vulnerability from an instance of ParseVulnerability 422 | 423 | : param: parsed_vuln: parsed openvas xml element 424 | : type: ParseVulnerability 425 | 426 | raises TypeError 427 | """ 428 | if not isinstance(parsed_vuln, ParseVulnerability): 429 | raise TypeError("Expected ParseVulnerability, got '{}' instead".format(type(parsed_vuln))) 430 | 431 | # check if a vulnerability with the same vuln_id (nvt-oid) already exists in the vuln_list 432 | for v in self.vuln_list: 433 | if v.vuln_id == parsed_vuln.vuln_id: 434 | return 435 | 436 | v = Vulnerability(parsed_vuln.vuln_id, 437 | name=parsed_vuln.vuln_name, 438 | version=parsed_vuln.vuln_version, 439 | threat=parsed_vuln.vuln_threat, 440 | tags=parsed_vuln.vuln_tags, 441 | cvss=parsed_vuln.vuln_cvss, 442 | cves=parsed_vuln.vuln_cves, 443 | references=parsed_vuln.vuln_references, 444 | family=parsed_vuln.vuln_family, 445 | level=parsed_vuln.vuln_level) 446 | try: 447 | # added results to port function as will ne unique per port on each host. 448 | port = Port.string2port(parsed_vuln.vuln_port, parsed_vuln.vuln_result) 449 | except ValueError: 450 | port = Port("", "", "") 451 | v.add_vuln_host(self, port) 452 | self.vuln_list.append(v) 453 | self.num_vulns += 1 454 | self.nv[v.level] += 1 455 | self.sum_cvss += v.cvss 456 | if v.cvss > self.higher_cvss: 457 | self.higher_cvss = v.cvss 458 | 459 | def nv_total(self): 460 | return self.nv['critical'] + self.nv['high'] + self.nv['medium'] + self.nv['low'] 461 | 462 | def __eq__(self, other:'Host'): 463 | return ( 464 | isinstance(other, Host) and 465 | other.ip == self.ip and 466 | other.host_name == self.host_name 467 | ) 468 | 469 | class Vulnerability(object): 470 | """Vulnerability information""" 471 | 472 | def __init__(self, vuln_id:str, name:str, threat:str, **kwargs): 473 | """ 474 | :param vuln_id: OpenVAS plugin id 475 | :type vuln_id: basestring 476 | 477 | :param name: Vulnerability name 478 | :type name: str 479 | 480 | :param threat: Threat type: None, Low, Medium, High 481 | :type threat: str 482 | 483 | :param cves: list of CVEs 484 | :type cves: list(str) 485 | 486 | :param cvss: CVSS number value 487 | :type cvss: float 488 | 489 | :param level: Threat level according to CVSS: None, Low, Medium, High, Critical 490 | :type level: str 491 | 492 | :param tags: vulnerability tags 493 | :type tags: dict 494 | 495 | :param references: list of references 496 | :type references: list(str) 497 | 498 | :param family: Vulnerability family 499 | :type family: str 500 | 501 | :param result: Vulnerability result 502 | :type description: str 503 | 504 | :raises: TypeError, ValueError 505 | """ 506 | # Get info 507 | cves: list[str] = kwargs.get("cves", list()) or list() 508 | cvss: float = kwargs.get("cvss", -1.0) or -1.0 509 | level: str = kwargs.get("level", "None") or "None" 510 | tags: dict = kwargs.get("tags", dict()) or dict() 511 | references: str = kwargs.get("references", "Unknown") or "Unknown" 512 | family: str = kwargs.get("family", "Unknown") or "Unknown" 513 | result: str = kwargs.get("description", "Unknown") or "Unknown" 514 | 515 | if not isinstance(vuln_id, str): 516 | raise TypeError("Expected basestring, got '{}' instead".format(type(vuln_id))) 517 | if not isinstance(name, str): 518 | raise TypeError("Expected basestring, got '{}' instead".format(type(name))) 519 | if not isinstance(threat, str): 520 | raise TypeError("Expected basestring, got '{}' instead".format(type(threat))) 521 | if not isinstance(family, str): 522 | raise TypeError("Expected basestring, got '{}' instead".format(type(family))) 523 | if not isinstance(result, str): 524 | raise TypeError("Expected basestring, got '{}' instead".format(type(result))) 525 | if not isinstance(cves, list): 526 | raise TypeError("Expected list, got '{}' instead".format(type(cves))) 527 | else: 528 | for x in cves: 529 | if not isinstance(x, str): 530 | raise TypeError("Expected basestring, got '{}' instead".format(type(x))) 531 | 532 | if not isinstance(cvss, (float, int)): 533 | raise TypeError("Expected float, got '{}' instead".format(type(cvss))) 534 | if not isinstance(level, str): 535 | raise TypeError("Expected basestring, got '{}' instead".format(type(level))) 536 | if not isinstance(tags, dict): 537 | raise TypeError("Expected dict, got '{}' instead".format(type(tags))) 538 | if not isinstance(references, str): 539 | raise TypeError("Expected string, got '{}' instead".format(type(references))) 540 | 541 | 542 | self.vuln_id = vuln_id 543 | self.name = name 544 | self.cves = cves 545 | self.cvss = float(cvss) 546 | self.level = level 547 | self.description:str = tags.get('summary', '') 548 | self.detect:str = tags.get('vuldetect', '') 549 | self.insight:str = tags.get('insight', '') 550 | self.version:str = kwargs.get("version") or '' 551 | self.impact:str = tags.get('impact', '') 552 | self.affected:str = tags.get('affected', '') 553 | self.solution:str = tags.get('solution', '') 554 | self.solution_type:str = tags.get('solution_type', '') 555 | self.references = references 556 | self.threat = threat 557 | self.family = family 558 | self.result = result 559 | # Hosts 560 | self.hosts:list[tuple[Host, Port]] = [] 561 | 562 | def add_vuln_host(self, host:Host, port:Port): 563 | """ 564 | Add a host and a port associated to this vulnerability 565 | 566 | :param host: Host instance 567 | :type host: Host 568 | 569 | :param port: Port instance 570 | :type port: Port 571 | 572 | :raises: TypeError 573 | """ 574 | if not isinstance(host, Host): 575 | raise TypeError("Expected Host, got '{}' instead".format(type(host))) 576 | if port is not None: 577 | if not isinstance(port, Port): 578 | raise TypeError("Expected Port, got '{}' instead".format(type(port))) 579 | 580 | if (host, port) not in self.hosts: 581 | self.hosts.append((host, port)) 582 | 583 | def __eq__(self, other:'Vulnerability'): 584 | if not isinstance(other, Vulnerability): 585 | raise TypeError("Expected Vulnerability, got '{}' instead".format(type(other))) 586 | 587 | if ( 588 | other.vuln_id != self.vuln_id or 589 | other.name != self.name or 590 | other.cves != self.cves or 591 | other.cvss != self.cvss or 592 | other.level != self.level or 593 | other.description != self.description or 594 | other.detect != self.detect or 595 | other.version != self.version or 596 | other.insight != self.insight or 597 | other.impact != self.impact or 598 | other.affected != self.affected or 599 | other.solution != self.solution or 600 | other.solution_type != self.solution_type or 601 | other.references != self.references or 602 | other.threat != self.threat or 603 | other.family != self.family or 604 | other.result != self.result 605 | ): 606 | return False 607 | 608 | for host, port in self.hosts: 609 | for o_host, o_port in other.hosts: 610 | if o_host != host or o_port != port: 611 | return False 612 | 613 | return True 614 | 615 | class ResultTree(dict): 616 | """ 617 | A dict of Hosts instances 618 | """ 619 | 620 | def addresult(self, parsed_vuln: ParseVulnerability): 621 | """ 622 | Adds a new vulnerability to an existing Host instance or creates one 623 | 624 | : param: parsed_vuln: parsed openvas xml element 625 | : type: ParseVulnerability 626 | 627 | raises TypeError 628 | """ 629 | if not isinstance(parsed_vuln, ParseVulnerability): 630 | raise TypeError("Expected ParseVulnerability, got '{}' instead".format(type(parsed_vuln))) 631 | 632 | hostip = parsed_vuln.vuln_host 633 | try: 634 | self[hostip].addvulnerability(parsed_vuln) 635 | except KeyError: 636 | self[hostip] = Host(hostip, parsed_vuln.vuln_host_name) 637 | self[hostip].addvulnerability(parsed_vuln) 638 | 639 | def sortedbysumcvss(self): 640 | """ 641 | Returns a dict of keys and sum of cvss severity ordered by sum of cvss severity 642 | """ 643 | temp_dict = {} 644 | for key in self: 645 | temp_dict[key] = (self[key].higher_cvss, self[key].sum_cvss) 646 | s = list({key: v1 for key, v1 in sorted(temp_dict.items(), key=lambda x: (x[1], x[0]), reverse = True)}.keys()) 647 | return s 648 | 649 | def sortedbynumvulnerabilities(self): 650 | """ 651 | Returns a dict of keys and number of vulnerabilities ordered by number of vulnerabilities 652 | """ 653 | temp_dict = {} 654 | for key in self: 655 | temp_dict[key] = self[key].num_vulns 656 | # s = res = {key: val for key, val in sorted(temp_dict.items(), key = lambda ele: ele[1], reverse = True)} 657 | return {key: val for key, val in sorted(temp_dict.items(), key = lambda ele: ele[1], reverse = True)} 658 | 659 | def sorted_keys_by_rank(self): 660 | """ 661 | Returns a list of keys of self reverse ordered by rank. 'Rank' here emulates 662 | the order used at openvas' host tab in the report page of a task: 663 | higher_cvss -> # critical vulns -> # high vulns -> # medium vulns -> # low vulns 664 | """ 665 | temp_list = [] 666 | for key in self: 667 | temp_list.append((self[key].nv['low'], self[key].nv['medium'], self[key].nv['high'], 668 | self[key].nv['critical'], self[key].higher_cvss, key)) 669 | s = [v[5] for v in sorted(temp_list, 670 | key = lambda x: (x[4], x[3], x[2], x[1], x[0]), 671 | reverse=True)] 672 | return s 673 | 674 | 675 | 676 | 677 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Based on OpenVAS2Report (https://github.com/cr0hn/openvas_to_report) 2 | by cr0hn (@ggdaniel). 3 | Original license below. 4 | 5 | -------------------------------------------------------------------------------- 6 | 7 | Copyright (c) TheGroundZero (@DezeStijn) All rights reserved. 8 | 9 | GNU GENERAL PUBLIC LICENSE 10 | Version 3, 29 June 2007 11 | 12 | Copyright (C) 2007 Free Software Foundation, Inc. 13 | Everyone is permitted to copy and distribute verbatim copies 14 | of this license document, but changing it is not allowed. 15 | 16 | Preamble 17 | 18 | The GNU General Public License is a free, copyleft license for 19 | software and other kinds of works. 20 | 21 | The licenses for most software and other practical works are designed 22 | to take away your freedom to share and change the works. By contrast, 23 | the GNU General Public License is intended to guarantee your freedom to 24 | share and change all versions of a program--to make sure it remains free 25 | software for all its users. We, the Free Software Foundation, use the 26 | GNU General Public License for most of our software; it applies also to 27 | any other work released this way by its authors. You can apply it to 28 | your programs, too. 29 | 30 | When we speak of free software, we are referring to freedom, not 31 | price. Our General Public Licenses are designed to make sure that you 32 | have the freedom to distribute copies of free software (and charge for 33 | them if you wish), that you receive source code or can get it if you 34 | want it, that you can change the software or use pieces of it in new 35 | free programs, and that you know you can do these things. 36 | 37 | To protect your rights, we need to prevent others from denying you 38 | these rights or asking you to surrender the rights. Therefore, you have 39 | certain responsibilities if you distribute copies of the software, or if 40 | you modify it: responsibilities to respect the freedom of others. 41 | 42 | For example, if you distribute copies of such a program, whether 43 | gratis or for a fee, you must pass on to the recipients the same 44 | freedoms that you received. You must make sure that they, too, receive 45 | or can get the source code. And you must show them these terms so they 46 | know their rights. 47 | 48 | Developers that use the GNU GPL protect your rights with two steps: 49 | (1) assert copyright on the software, and (2) offer you this License 50 | giving you legal permission to copy, distribute and/or modify it. 51 | 52 | For the developers' and authors' protection, the GPL clearly explains 53 | that there is no warranty for this free software. For both users' and 54 | authors' sake, the GPL requires that modified versions be marked as 55 | changed, so that their problems will not be attributed erroneously to 56 | authors of previous versions. 57 | 58 | Some devices are designed to deny users access to install or run 59 | modified versions of the software inside them, although the manufacturer 60 | can do so. This is fundamentally incompatible with the aim of 61 | protecting users' freedom to change the software. The systematic 62 | pattern of such abuse occurs in the area of products for individuals to 63 | use, which is precisely where it is most unacceptable. Therefore, we 64 | have designed this version of the GPL to prohibit the practice for those 65 | products. If such problems arise substantially in other domains, we 66 | stand ready to extend this provision to those domains in future versions 67 | of the GPL, as needed to protect the freedom of users. 68 | 69 | Finally, every program is threatened constantly by software patents. 70 | States should not allow patents to restrict development and use of 71 | software on general-purpose computers, but in those that do, we wish to 72 | avoid the special danger that patents applied to a free program could 73 | make it effectively proprietary. To prevent this, the GPL assures that 74 | patents cannot be used to render the program non-free. 75 | 76 | The precise terms and conditions for copying, distribution and 77 | modification follow. 78 | 79 | TERMS AND CONDITIONS 80 | 81 | 0. Definitions. 82 | 83 | "This License" refers to version 3 of the GNU General Public License. 84 | 85 | "Copyright" also means copyright-like laws that apply to other kinds of 86 | works, such as semiconductor masks. 87 | 88 | "The Program" refers to any copyrightable work licensed under this 89 | License. Each licensee is addressed as "you". "Licensees" and 90 | "recipients" may be individuals or organizations. 91 | 92 | To "modify" a work means to copy from or adapt all or part of the work 93 | in a fashion requiring copyright permission, other than the making of an 94 | exact copy. The resulting work is called a "modified version" of the 95 | earlier work or a work "based on" the earlier work. 96 | 97 | A "covered work" means either the unmodified Program or a work based 98 | on the Program. 99 | 100 | To "propagate" a work means to do anything with it that, without 101 | permission, would make you directly or secondarily liable for 102 | infringement under applicable copyright law, except executing it on a 103 | computer or modifying a private copy. Propagation includes copying, 104 | distribution (with or without modification), making available to the 105 | public, and in some countries other activities as well. 106 | 107 | To "convey" a work means any kind of propagation that enables other 108 | parties to make or receive copies. Mere interaction with a user through 109 | a computer network, with no transfer of a copy, is not conveying. 110 | 111 | An interactive user interface displays "Appropriate Legal Notices" 112 | to the extent that it includes a convenient and prominently visible 113 | feature that (1) displays an appropriate copyright notice, and (2) 114 | tells the user that there is no warranty for the work (except to the 115 | extent that warranties are provided), that licensees may convey the 116 | work under this License, and how to view a copy of this License. If 117 | the interface presents a list of user commands or options, such as a 118 | menu, a prominent item in the list meets this criterion. 119 | 120 | 1. Source Code. 121 | 122 | The "source code" for a work means the preferred form of the work 123 | for making modifications to it. "Object code" means any non-source 124 | form of a work. 125 | 126 | A "Standard Interface" means an interface that either is an official 127 | standard defined by a recognized standards body, or, in the case of 128 | interfaces specified for a particular programming language, one that 129 | is widely used among developers working in that language. 130 | 131 | The "System Libraries" of an executable work include anything, other 132 | than the work as a whole, that (a) is included in the normal form of 133 | packaging a Major Component, but which is not part of that Major 134 | Component, and (b) serves only to enable use of the work with that 135 | Major Component, or to implement a Standard Interface for which an 136 | implementation is available to the public in source code form. A 137 | "Major Component", in this context, means a major essential component 138 | (kernel, window system, and so on) of the specific operating system 139 | (if any) on which the executable work runs, or a compiler used to 140 | produce the work, or an object code interpreter used to run it. 141 | 142 | The "Corresponding Source" for a work in object code form means all 143 | the source code needed to generate, install, and (for an executable 144 | work) run the object code and to modify the work, including scripts to 145 | control those activities. However, it does not include the work's 146 | System Libraries, or general-purpose tools or generally available free 147 | programs which are used unmodified in performing those activities but 148 | which are not part of the work. For example, Corresponding Source 149 | includes interface definition files associated with source files for 150 | the work, and the source code for shared libraries and dynamically 151 | linked subprograms that the work is specifically designed to require, 152 | such as by intimate data communication or control flow between those 153 | subprograms and other parts of the work. 154 | 155 | The Corresponding Source need not include anything that users 156 | can regenerate automatically from other parts of the Corresponding 157 | Source. 158 | 159 | The Corresponding Source for a work in source code form is that 160 | same work. 161 | 162 | 2. Basic Permissions. 163 | 164 | All rights granted under this License are granted for the term of 165 | copyright on the Program, and are irrevocable provided the stated 166 | conditions are met. This License explicitly affirms your unlimited 167 | permission to run the unmodified Program. The output from running a 168 | covered work is covered by this License only if the output, given its 169 | content, constitutes a covered work. This License acknowledges your 170 | rights of fair use or other equivalent, as provided by copyright law. 171 | 172 | You may make, run and propagate covered works that you do not 173 | convey, without conditions so long as your license otherwise remains 174 | in force. You may convey covered works to others for the sole purpose 175 | of having them make modifications exclusively for you, or provide you 176 | with facilities for running those works, provided that you comply with 177 | the terms of this License in conveying all material for which you do 178 | not control copyright. Those thus making or running the covered works 179 | for you must do so exclusively on your behalf, under your direction 180 | and control, on terms that prohibit them from making any copies of 181 | your copyrighted material outside their relationship with you. 182 | 183 | Conveying under any other circumstances is permitted solely under 184 | the conditions stated below. Sublicensing is not allowed; section 10 185 | makes it unnecessary. 186 | 187 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 188 | 189 | No covered work shall be deemed part of an effective technological 190 | measure under any applicable law fulfilling obligations under article 191 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 192 | similar laws prohibiting or restricting circumvention of such 193 | measures. 194 | 195 | When you convey a covered work, you waive any legal power to forbid 196 | circumvention of technological measures to the extent such circumvention 197 | is effected by exercising rights under this License with respect to 198 | the covered work, and you disclaim any intention to limit operation or 199 | modification of the work as a means of enforcing, against the work's 200 | users, your or third parties' legal rights to forbid circumvention of 201 | technological measures. 202 | 203 | 4. Conveying Verbatim Copies. 204 | 205 | You may convey verbatim copies of the Program's source code as you 206 | receive it, in any medium, provided that you conspicuously and 207 | appropriately publish on each copy an appropriate copyright notice; 208 | keep intact all notices stating that this License and any 209 | non-permissive terms added in accord with section 7 apply to the code; 210 | keep intact all notices of the absence of any warranty; and give all 211 | recipients a copy of this License along with the Program. 212 | 213 | You may charge any price or no price for each copy that you convey, 214 | and you may offer support or warranty protection for a fee. 215 | 216 | 5. Conveying Modified Source Versions. 217 | 218 | You may convey a work based on the Program, or the modifications to 219 | produce it from the Program, in the form of source code under the 220 | terms of section 4, provided that you also meet all of these conditions: 221 | 222 | a) The work must carry prominent notices stating that you modified 223 | it, and giving a relevant date. 224 | 225 | b) The work must carry prominent notices stating that it is 226 | released under this License and any conditions added under section 227 | 7. This requirement modifies the requirement in section 4 to 228 | "keep intact all notices". 229 | 230 | c) You must license the entire work, as a whole, under this 231 | License to anyone who comes into possession of a copy. This 232 | License will therefore apply, along with any applicable section 7 233 | additional terms, to the whole of the work, and all its parts, 234 | regardless of how they are packaged. This License gives no 235 | permission to license the work in any other way, but it does not 236 | invalidate such permission if you have separately received it. 237 | 238 | d) If the work has interactive user interfaces, each must display 239 | Appropriate Legal Notices; however, if the Program has interactive 240 | interfaces that do not display Appropriate Legal Notices, your 241 | work need not make them do so. 242 | 243 | A compilation of a covered work with other separate and independent 244 | works, which are not by their nature extensions of the covered work, 245 | and which are not combined with it such as to form a larger program, 246 | in or on a volume of a storage or distribution medium, is called an 247 | "aggregate" if the compilation and its resulting copyright are not 248 | used to limit the access or legal rights of the compilation's users 249 | beyond what the individual works permit. Inclusion of a covered work 250 | in an aggregate does not cause this License to apply to the other 251 | parts of the aggregate. 252 | 253 | 6. Conveying Non-Source Forms. 254 | 255 | You may convey a covered work in object code form under the terms 256 | of sections 4 and 5, provided that you also convey the 257 | machine-readable Corresponding Source under the terms of this License, 258 | in one of these ways: 259 | 260 | a) Convey the object code in, or embodied in, a physical product 261 | (including a physical distribution medium), accompanied by the 262 | Corresponding Source fixed on a durable physical medium 263 | customarily used for software interchange. 264 | 265 | b) Convey the object code in, or embodied in, a physical product 266 | (including a physical distribution medium), accompanied by a 267 | written offer, valid for at least three years and valid for as 268 | long as you offer spare parts or customer support for that product 269 | model, to give anyone who possesses the object code either (1) a 270 | copy of the Corresponding Source for all the software in the 271 | product that is covered by this License, on a durable physical 272 | medium customarily used for software interchange, for a price no 273 | more than your reasonable cost of physically performing this 274 | conveying of source, or (2) access to copy the 275 | Corresponding Source from a network server at no charge. 276 | 277 | c) Convey individual copies of the object code with a copy of the 278 | written offer to provide the Corresponding Source. This 279 | alternative is allowed only occasionally and noncommercially, and 280 | only if you received the object code with such an offer, in accord 281 | with subsection 6b. 282 | 283 | d) Convey the object code by offering access from a designated 284 | place (gratis or for a charge), and offer equivalent access to the 285 | Corresponding Source in the same way through the same place at no 286 | further charge. You need not require recipients to copy the 287 | Corresponding Source along with the object code. If the place to 288 | copy the object code is a network server, the Corresponding Source 289 | may be on a different server (operated by you or a third party) 290 | that supports equivalent copying facilities, provided you maintain 291 | clear directions next to the object code saying where to find the 292 | Corresponding Source. Regardless of what server hosts the 293 | Corresponding Source, you remain obligated to ensure that it is 294 | available for as long as needed to satisfy these requirements. 295 | 296 | e) Convey the object code using peer-to-peer transmission, provided 297 | you inform other peers where the object code and Corresponding 298 | Source of the work are being offered to the general public at no 299 | charge under subsection 6d. 300 | 301 | A separable portion of the object code, whose source code is excluded 302 | from the Corresponding Source as a System Library, need not be 303 | included in conveying the object code work. 304 | 305 | A "User Product" is either (1) a "consumer product", which means any 306 | tangible personal property which is normally used for personal, family, 307 | or household purposes, or (2) anything designed or sold for incorporation 308 | into a dwelling. In determining whether a product is a consumer product, 309 | doubtful cases shall be resolved in favor of coverage. For a particular 310 | product received by a particular user, "normally used" refers to a 311 | typical or common use of that class of product, regardless of the status 312 | of the particular user or of the way in which the particular user 313 | actually uses, or expects or is expected to use, the product. A product 314 | is a consumer product regardless of whether the product has substantial 315 | commercial, industrial or non-consumer uses, unless such uses represent 316 | the only significant mode of use of the product. 317 | 318 | "Installation Information" for a User Product means any methods, 319 | procedures, authorization keys, or other information required to install 320 | and execute modified versions of a covered work in that User Product from 321 | a modified version of its Corresponding Source. The information must 322 | suffice to ensure that the continued functioning of the modified object 323 | code is in no case prevented or interfered with solely because 324 | modification has been made. 325 | 326 | If you convey an object code work under this section in, or with, or 327 | specifically for use in, a User Product, and the conveying occurs as 328 | part of a transaction in which the right of possession and use of the 329 | User Product is transferred to the recipient in perpetuity or for a 330 | fixed term (regardless of how the transaction is characterized), the 331 | Corresponding Source conveyed under this section must be accompanied 332 | by the Installation Information. But this requirement does not apply 333 | if neither you nor any third party retains the ability to install 334 | modified object code on the User Product (for example, the work has 335 | been installed in ROM). 336 | 337 | The requirement to provide Installation Information does not include a 338 | requirement to continue to provide support service, warranty, or updates 339 | for a work that has been modified or installed by the recipient, or for 340 | the User Product in which it has been modified or installed. Access to a 341 | network may be denied when the modification itself materially and 342 | adversely affects the operation of the network or violates the rules and 343 | protocols for communication across the network. 344 | 345 | Corresponding Source conveyed, and Installation Information provided, 346 | in accord with this section must be in a format that is publicly 347 | documented (and with an implementation available to the public in 348 | source code form), and must require no special password or key for 349 | unpacking, reading or copying. 350 | 351 | 7. Additional Terms. 352 | 353 | "Additional permissions" are terms that supplement the terms of this 354 | License by making exceptions from one or more of its conditions. 355 | Additional permissions that are applicable to the entire Program shall 356 | be treated as though they were included in this License, to the extent 357 | that they are valid under applicable law. If additional permissions 358 | apply only to part of the Program, that part may be used separately 359 | under those permissions, but the entire Program remains governed by 360 | this License without regard to the additional permissions. 361 | 362 | When you convey a copy of a covered work, you may at your option 363 | remove any additional permissions from that copy, or from any part of 364 | it. (Additional permissions may be written to require their own 365 | removal in certain cases when you modify the work.) You may place 366 | additional permissions on material, added by you to a covered work, 367 | for which you have or can give appropriate copyright permission. 368 | 369 | Notwithstanding any other provision of this License, for material you 370 | add to a covered work, you may (if authorized by the copyright holders of 371 | that material) supplement the terms of this License with terms: 372 | 373 | a) Disclaiming warranty or limiting liability differently from the 374 | terms of sections 15 and 16 of this License; or 375 | 376 | b) Requiring preservation of specified reasonable legal notices or 377 | author attributions in that material or in the Appropriate Legal 378 | Notices displayed by works containing it; or 379 | 380 | c) Prohibiting misrepresentation of the origin of that material, or 381 | requiring that modified versions of such material be marked in 382 | reasonable ways as different from the original version; or 383 | 384 | d) Limiting the use for publicity purposes of names of licensors or 385 | authors of the material; or 386 | 387 | e) Declining to grant rights under trademark law for use of some 388 | trade names, trademarks, or service marks; or 389 | 390 | f) Requiring indemnification of licensors and authors of that 391 | material by anyone who conveys the material (or modified versions of 392 | it) with contractual assumptions of liability to the recipient, for 393 | any liability that these contractual assumptions directly impose on 394 | those licensors and authors. 395 | 396 | All other non-permissive additional terms are considered "further 397 | restrictions" within the meaning of section 10. If the Program as you 398 | received it, or any part of it, contains a notice stating that it is 399 | governed by this License along with a term that is a further 400 | restriction, you may remove that term. If a license document contains 401 | a further restriction but permits relicensing or conveying under this 402 | License, you may add to a covered work material governed by the terms 403 | of that license document, provided that the further restriction does 404 | not survive such relicensing or conveying. 405 | 406 | If you add terms to a covered work in accord with this section, you 407 | must place, in the relevant source files, a statement of the 408 | additional terms that apply to those files, or a notice indicating 409 | where to find the applicable terms. 410 | 411 | Additional terms, permissive or non-permissive, may be stated in the 412 | form of a separately written license, or stated as exceptions; 413 | the above requirements apply either way. 414 | 415 | 8. Termination. 416 | 417 | You may not propagate or modify a covered work except as expressly 418 | provided under this License. Any attempt otherwise to propagate or 419 | modify it is void, and will automatically terminate your rights under 420 | this License (including any patent licenses granted under the third 421 | paragraph of section 11). 422 | 423 | However, if you cease all violation of this License, then your 424 | license from a particular copyright holder is reinstated (a) 425 | provisionally, unless and until the copyright holder explicitly and 426 | finally terminates your license, and (b) permanently, if the copyright 427 | holder fails to notify you of the violation by some reasonable means 428 | prior to 60 days after the cessation. 429 | 430 | Moreover, your license from a particular copyright holder is 431 | reinstated permanently if the copyright holder notifies you of the 432 | violation by some reasonable means, this is the first time you have 433 | received notice of violation of this License (for any work) from that 434 | copyright holder, and you cure the violation prior to 30 days after 435 | your receipt of the notice. 436 | 437 | Termination of your rights under this section does not terminate the 438 | licenses of parties who have received copies or rights from you under 439 | this License. If your rights have been terminated and not permanently 440 | reinstated, you do not qualify to receive new licenses for the same 441 | material under section 10. 442 | 443 | 9. Acceptance Not Required for Having Copies. 444 | 445 | You are not required to accept this License in order to receive or 446 | run a copy of the Program. Ancillary propagation of a covered work 447 | occurring solely as a consequence of using peer-to-peer transmission 448 | to receive a copy likewise does not require acceptance. However, 449 | nothing other than this License grants you permission to propagate or 450 | modify any covered work. These actions infringe copyright if you do 451 | not accept this License. Therefore, by modifying or propagating a 452 | covered work, you indicate your acceptance of this License to do so. 453 | 454 | 10. Automatic Licensing of Downstream Recipients. 455 | 456 | Each time you convey a covered work, the recipient automatically 457 | receives a license from the original licensors, to run, modify and 458 | propagate that work, subject to this License. You are not responsible 459 | for enforcing compliance by third parties with this License. 460 | 461 | An "entity transaction" is a transaction transferring control of an 462 | organization, or substantially all assets of one, or subdividing an 463 | organization, or merging organizations. If propagation of a covered 464 | work results from an entity transaction, each party to that 465 | transaction who receives a copy of the work also receives whatever 466 | licenses to the work the party's predecessor in interest had or could 467 | give under the previous paragraph, plus a right to possession of the 468 | Corresponding Source of the work from the predecessor in interest, if 469 | the predecessor has it or can get it with reasonable efforts. 470 | 471 | You may not impose any further restrictions on the exercise of the 472 | rights granted or affirmed under this License. For example, you may 473 | not impose a license fee, royalty, or other charge for exercise of 474 | rights granted under this License, and you may not initiate litigation 475 | (including a cross-claim or counterclaim in a lawsuit) alleging that 476 | any patent claim is infringed by making, using, selling, offering for 477 | sale, or importing the Program or any portion of it. 478 | 479 | 11. Patents. 480 | 481 | A "contributor" is a copyright holder who authorizes use under this 482 | License of the Program or a work on which the Program is based. The 483 | work thus licensed is called the contributor's "contributor version". 484 | 485 | A contributor's "essential patent claims" are all patent claims 486 | owned or controlled by the contributor, whether already acquired or 487 | hereafter acquired, that would be infringed by some manner, permitted 488 | by this License, of making, using, or selling its contributor version, 489 | but do not include claims that would be infringed only as a 490 | consequence of further modification of the contributor version. For 491 | purposes of this definition, "control" includes the right to grant 492 | patent sublicenses in a manner consistent with the requirements of 493 | this License. 494 | 495 | Each contributor grants you a non-exclusive, worldwide, royalty-free 496 | patent license under the contributor's essential patent claims, to 497 | make, use, sell, offer for sale, import and otherwise run, modify and 498 | propagate the contents of its contributor version. 499 | 500 | In the following three paragraphs, a "patent license" is any express 501 | agreement or commitment, however denominated, not to enforce a patent 502 | (such as an express permission to practice a patent or covenant not to 503 | sue for patent infringement). To "grant" such a patent license to a 504 | party means to make such an agreement or commitment not to enforce a 505 | patent against the party. 506 | 507 | If you convey a covered work, knowingly relying on a patent license, 508 | and the Corresponding Source of the work is not available for anyone 509 | to copy, free of charge and under the terms of this License, through a 510 | publicly available network server or other readily accessible means, 511 | then you must either (1) cause the Corresponding Source to be so 512 | available, or (2) arrange to deprive yourself of the benefit of the 513 | patent license for this particular work, or (3) arrange, in a manner 514 | consistent with the requirements of this License, to extend the patent 515 | license to downstream recipients. "Knowingly relying" means you have 516 | actual knowledge that, but for the patent license, your conveying the 517 | covered work in a country, or your recipient's use of the covered work 518 | in a country, would infringe one or more identifiable patents in that 519 | country that you have reason to believe are valid. 520 | 521 | If, pursuant to or in connection with a single transaction or 522 | arrangement, you convey, or propagate by procuring conveyance of, a 523 | covered work, and grant a patent license to some of the parties 524 | receiving the covered work authorizing them to use, propagate, modify 525 | or convey a specific copy of the covered work, then the patent license 526 | you grant is automatically extended to all recipients of the covered 527 | work and works based on it. 528 | 529 | A patent license is "discriminatory" if it does not include within 530 | the scope of its coverage, prohibits the exercise of, or is 531 | conditioned on the non-exercise of one or more of the rights that are 532 | specifically granted under this License. You may not convey a covered 533 | work if you are a party to an arrangement with a third party that is 534 | in the business of distributing software, under which you make payment 535 | to the third party based on the extent of your activity of conveying 536 | the work, and under which the third party grants, to any of the 537 | parties who would receive the covered work from you, a discriminatory 538 | patent license (a) in connection with copies of the covered work 539 | conveyed by you (or copies made from those copies), or (b) primarily 540 | for and in connection with specific products or compilations that 541 | contain the covered work, unless you entered into that arrangement, 542 | or that patent license was granted, prior to 28 March 2007. 543 | 544 | Nothing in this License shall be construed as excluding or limiting 545 | any implied license or other defenses to infringement that may 546 | otherwise be available to you under applicable patent law. 547 | 548 | 12. No Surrender of Others' Freedom. 549 | 550 | If conditions are imposed on you (whether by court order, agreement or 551 | otherwise) that contradict the conditions of this License, they do not 552 | excuse you from the conditions of this License. If you cannot convey a 553 | covered work so as to satisfy simultaneously your obligations under this 554 | License and any other pertinent obligations, then as a consequence you may 555 | not convey it at all. For example, if you agree to terms that obligate you 556 | to collect a royalty for further conveying from those to whom you convey 557 | the Program, the only way you could satisfy both those terms and this 558 | License would be to refrain entirely from conveying the Program. 559 | 560 | 13. Use with the GNU Affero General Public License. 561 | 562 | Notwithstanding any other provision of this License, you have 563 | permission to link or combine any covered work with a work licensed 564 | under version 3 of the GNU Affero General Public License into a single 565 | combined work, and to convey the resulting work. The terms of this 566 | License will continue to apply to the part which is the covered work, 567 | but the special requirements of the GNU Affero General Public License, 568 | section 13, concerning interaction through a network will apply to the 569 | combination as such. 570 | 571 | 14. Revised Versions of this License. 572 | 573 | The Free Software Foundation may publish revised and/or new versions of 574 | the GNU General Public License from time to time. Such new versions will 575 | be similar in spirit to the present version, but may differ in detail to 576 | address new problems or concerns. 577 | 578 | Each version is given a distinguishing version number. If the 579 | Program specifies that a certain numbered version of the GNU General 580 | Public License "or any later version" applies to it, you have the 581 | option of following the terms and conditions either of that numbered 582 | version or of any later version published by the Free Software 583 | Foundation. If the Program does not specify a version number of the 584 | GNU General Public License, you may choose any version ever published 585 | by the Free Software Foundation. 586 | 587 | If the Program specifies that a proxy can decide which future 588 | versions of the GNU General Public License can be used, that proxy's 589 | public statement of acceptance of a version permanently authorizes you 590 | to choose that version for the Program. 591 | 592 | Later license versions may give you additional or different 593 | permissions. However, no additional obligations are imposed on any 594 | author or copyright holder as a result of your choosing to follow a 595 | later version. 596 | 597 | 15. Disclaimer of Warranty. 598 | 599 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 600 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 601 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 602 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 603 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 604 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 605 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 606 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 607 | 608 | 16. Limitation of Liability. 609 | 610 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 611 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 612 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 613 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 614 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 615 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 616 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 617 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 618 | SUCH DAMAGES. 619 | 620 | 17. Interpretation of Sections 15 and 16. 621 | 622 | If the disclaimer of warranty and limitation of liability provided 623 | above cannot be given local legal effect according to their terms, 624 | reviewing courts shall apply local law that most closely approximates 625 | an absolute waiver of all civil liability in connection with the 626 | Program, unless a warranty or assumption of liability accompanies a 627 | copy of the Program in return for a fee. 628 | 629 | END OF TERMS AND CONDITIONS 630 | 631 | How to Apply These Terms to Your New Programs 632 | 633 | If you develop a new program, and you want it to be of the greatest 634 | possible use to the public, the best way to achieve this is to make it 635 | free software which everyone can redistribute and change under these terms. 636 | 637 | To do so, attach the following notices to the program. It is safest 638 | to attach them to the start of each source file to most effectively 639 | state the exclusion of warranty; and each file should have at least 640 | the "copyright" line and a pointer to where the full notice is found. 641 | 642 | 643 | Copyright (C) 644 | 645 | This program is free software: you can redistribute it and/or modify 646 | it under the terms of the GNU General Public License as published by 647 | the Free Software Foundation, either version 3 of the License, or 648 | (at your option) any later version. 649 | 650 | This program is distributed in the hope that it will be useful, 651 | but WITHOUT ANY WARRANTY; without even the implied warranty of 652 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 653 | GNU General Public License for more details. 654 | 655 | You should have received a copy of the GNU General Public License 656 | along with this program. If not, see . 657 | 658 | Also add information on how to contact you by electronic and paper mail. 659 | 660 | If the program does terminal interaction, make it output a short 661 | notice like this when it starts in an interactive mode: 662 | 663 | Copyright (C) 664 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 665 | This is free software, and you are welcome to redistribute it 666 | under certain conditions; type `show c' for details. 667 | 668 | The hypothetical commands `show w' and `show c' should show the appropriate 669 | parts of the General Public License. Of course, your program's commands 670 | might be different; for a GUI interface, you would use an "about box". 671 | 672 | You should also get your employer (if you work as a programmer) or school, 673 | if any, to sign a "copyright disclaimer" for the program, if necessary. 674 | For more information on this, and how to apply and follow the GNU GPL, see 675 | . 676 | 677 | The GNU General Public License does not permit incorporating your program 678 | into proprietary programs. If your program is a subroutine library, you 679 | may consider it more useful to permit linking proprietary applications with 680 | the library. If this is what you want to do, use the GNU Lesser General 681 | Public License instead of this License. But first, please read 682 | . 683 | 684 | -------------------------------------------------------------------------------- 685 | 686 | Original LICENSE of OpenVAS2Report (https://github.com/cr0hn/openvas_to_report) 687 | 688 | 689 | Copyright (c) cr0hn - cr0hn<-at->cr0hn.com (@ggdaniel) All rights reserved. 690 | 691 | Redistribution and use in source and binary forms, with or without modification, 692 | are permitted provided that the following conditions are met: 693 | 694 | 1. Redistributions of source code must retain the above copyright notice, 695 | this list of conditions and the following disclaimer. 696 | 697 | 2. Redistributions in binary form must reproduce the above copyright 698 | notice, this list of conditions and the following disclaimer in the 699 | documentation and/or other materials provided with the distribution. 700 | 701 | 3. Neither the name of cr0hn nor the names of its contributors may be used 702 | to endorse or promote products derived from this software without 703 | specific prior written permission. 704 | 705 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 706 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 707 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 708 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 709 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 710 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 711 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 712 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 713 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 714 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 715 | -------------------------------------------------------------------------------- /openvasreporting/libs/export.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 4 | # Project name: OpenVAS Reporting: A tool to convert OpenVAS XML reports into Excel files. 5 | # Project URL: https://github.com/TheGroundZero/openvasreporting 6 | 7 | import re 8 | 9 | from collections import Counter 10 | from typing import Callable 11 | 12 | from .config import Config 13 | from .parsed_data import ResultTree, Host, Vulnerability 14 | 15 | # DEBUG 16 | #import sys 17 | #import logging 18 | #logging.basicConfig(stream=sys.stderr, level=logging.WARNING, 19 | # format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S") 20 | 21 | 22 | def implemented_exporters() -> dict[str, Callable]: 23 | """ 24 | Enum-link instance containing references to already implemented exporter function 25 | 26 | > implemented_exporters()[key](param[s]) 27 | 28 | key is a concatenation of the report-type arg + '-' + format arg 29 | 30 | :return: Pointer to exporter function 31 | """ 32 | return { 33 | 'vulnerability-xlsx': export_to_excel_by_vuln, 34 | 'vulnerability-docx': export_to_word_by_vuln, 35 | 'vulnerability-csv': export_to_csv_by_vuln, 36 | 'host-xlsx': export_to_excel_by_host, 37 | 'host-csv': export_to_csv_by_host, 38 | 'summary-csv': export_summary_to_csv 39 | } 40 | 41 | def _get_collections(vuln_info:list[Vulnerability]) -> tuple[list[Vulnerability], Counter[str], Counter[str], Counter[str]]: 42 | """ 43 | Sort vulnerability list info according to CVSS (desc) and Name (asc). 44 | Provide collections to be used in export. 45 | 46 | :param vuln_info: Vulnerability list info 47 | :type vuln_info: list(Vulnerability) 48 | 49 | :return: vuln_info, vuln_levels, vuln_host_by_level, vuln_by_family 50 | :rtype vuln_info: list(Vulnerability) 51 | :rtype vuln_levels: Counter 52 | :rtype vuln_host_by_level: Counter 53 | :rtype vuln_by_family: Counter 54 | """ 55 | vuln_info.sort(key=lambda key: key.name) 56 | vuln_info.sort(key=lambda key: key.cvss, reverse=True) 57 | vuln_levels:Counter[str] = Counter() 58 | vuln_host_by_level:Counter[str] = Counter() 59 | vuln_by_family:Counter[str] = Counter() 60 | # collect host names 61 | vuln_hostcount_by_level:list[list[str]] =[[] for _ in range(5)] 62 | level_choices = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3, 'none': 4} 63 | 64 | for _, vuln in enumerate(vuln_info, 1): 65 | vuln_levels[vuln.level.lower()] += 1 66 | # add host names to list so we count unquie hosts per level 67 | level_index:int = level_choices[vuln.level.lower()] 68 | 69 | for _, (host, _) in enumerate(vuln.hosts, 1): 70 | if host.ip not in vuln_hostcount_by_level[level_index]: 71 | vuln_hostcount_by_level[level_index].append(host.ip) 72 | 73 | vuln_by_family[vuln.family] += 1 74 | 75 | # now count hosts per level and return 76 | for level in Config.levels().values(): 77 | vuln_host_by_level[level] = len([*set(vuln_hostcount_by_level[level_choices[level.lower()]])]) # Put host in a set to avoid doublon 78 | 79 | return vuln_info, vuln_levels, vuln_host_by_level, vuln_by_family 80 | 81 | 82 | def export_to_excel_by_vuln(vuln_info, template=None, output_file:str='openvas_report.xlsx'): 83 | """ 84 | Export vulnerabilities info in an Excel file. 85 | 86 | :param vuln_info: Vulnerability list info 87 | :type vuln_info: list(Vulnerability) 88 | :param template: Not supported in xlsx-output 89 | :type template: NoneType 90 | 91 | :param output_file: Filename of the Excel file 92 | :type output_file: str 93 | 94 | :raises: TypeError, NotImplementedError 95 | """ 96 | 97 | import xlsxwriter 98 | 99 | if not isinstance(vuln_info, list): 100 | raise TypeError("Expected list, got '{}' instead".format(type(vuln_info))) 101 | else: 102 | for x in vuln_info: 103 | if not isinstance(x, Vulnerability): 104 | raise TypeError("Expected Vulnerability, got '{}' instead".format(type(x))) 105 | if not isinstance(output_file, str): 106 | raise TypeError("Expected str, got '{}' instead".format(type(output_file))) 107 | else: 108 | if not output_file: 109 | raise ValueError("output_file must have a valid name.") 110 | # if template is not None: 111 | # raise NotImplementedError("Use of template is not supported in XSLX-output.") 112 | 113 | vuln_info, vuln_levels, vuln_host_by_level, vuln_by_family = _get_collections(vuln_info) 114 | 115 | # ==================== 116 | # FUNCTIONS 117 | # ==================== 118 | def __row_height(text, width): 119 | return (max((len(text) // width), text.count('\n')) + 1) * 15 120 | 121 | workbook = xlsxwriter.Workbook(output_file) 122 | 123 | workbook.set_properties({ 124 | 'title': output_file, 125 | 'subject': 'OpenVAS report', 126 | 'author': 'TheGroundZero', 127 | 'category': 'report', 128 | 'keywords': 'OpenVAS, report', 129 | 'comments': 'TheGroundZero (https://github.com/TheGroundZero)'}) 130 | 131 | # ==================== 132 | # FORMATTING 133 | # ==================== 134 | workbook.formats[0].set_font_name('Tahoma') 135 | 136 | format_sheet_title_content = workbook.add_format({'font_name': 'Tahoma', 'font_size': 12, 137 | 'font_color': Config.colors()['blue'], 'bold': True, 138 | 'align': 'center', 'valign': 'vcenter', 'border': 1}) 139 | format_table_titles = workbook.add_format({'font_name': 'Tahoma', 'font_size': 11, 140 | 'font_color': 'white', 'bold': True, 141 | 'align': 'center', 'valign': 'vcenter', 142 | 'border': 1, 143 | 'bg_color': Config.colors()['blue']}) 144 | format_table_cells = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 145 | 'align': 'left', 'valign': 'top', 146 | 'border': 1, 'text_wrap': 1}) 147 | format_align_center = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 148 | 'align': 'center', 'valign': 'top'}) 149 | format_align_border = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 150 | 'align': 'center', 'valign': 'top', 151 | 'border': 1, 'text_wrap': 1}) 152 | format_toc = { 153 | 'critical': workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 'font_color': 'white', 154 | 'align': 'center', 'valign': 'top', 155 | 'border': 1, 156 | 'bg_color': Config.colors()['critical']}), 157 | 'high': workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 'font_color': 'white', 158 | 'align': 'center', 'valign': 'top', 159 | 'border': 1, 'bg_color': Config.colors()['high']}), 160 | 'medium': workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 'font_color': 'white', 161 | 'align': 'center', 'valign': 'top', 162 | 'border': 1, 'bg_color': Config.colors()['medium']}), 163 | 'low': workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 'font_color': 'white', 164 | 'align': 'center', 'valign': 'top', 165 | 'border': 1, 'bg_color': Config.colors()['low']}), 166 | 'none': workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 'font_color': 'white', 167 | 'align': 'center', 'valign': 'top', 168 | 'border': 1, 'bg_color': Config.colors()['none']}) 169 | } 170 | 171 | # ==================== 172 | # SUMMARY SHEET 173 | # ==================== 174 | sheet_name = "Summary" 175 | ws_sum = workbook.add_worksheet(sheet_name) 176 | ws_sum.set_tab_color(Config.colors()['blue']) 177 | 178 | ws_sum.set_column("A:A", 7, format_align_center) 179 | ws_sum.set_column("B:B", 25, format_align_center) 180 | ws_sum.set_column("C:C", 24, format_align_center) 181 | ws_sum.set_column("D:D", 20, format_align_center) 182 | ws_sum.set_column("E:E", 20, format_align_center) 183 | ws_sum.set_column("F:F", 7, format_align_center) 184 | 185 | # -------------------- 186 | # VULN SUMMARY 187 | # -------------------- 188 | ws_sum.merge_range("B2:E2", "VULNERABILITY SUMMARY", format_sheet_title_content) # type: ignore 189 | ws_sum.write("B3", "Threat", format_table_titles) 190 | ws_sum.write("C3", "Unique Vulns", format_table_titles) 191 | ws_sum.write("D3", "Hosts affected", format_table_titles) 192 | ws_sum.write("E3", "Discovered", format_table_titles) 193 | 194 | for i, level in enumerate(Config.levels().values(), 4): 195 | ws_sum.write("B{}".format(i), level.capitalize(), format_sheet_title_content) 196 | ws_sum.write("C{}".format(i), vuln_levels[level], format_align_border) 197 | ws_sum.write("D{}".format(i), vuln_host_by_level[level], format_align_border) 198 | ws_sum.write("E{}".format(i), vuln_levels[level] * vuln_host_by_level[level], format_align_border) 199 | 200 | ws_sum.write("B9", "Total", format_table_titles) 201 | ws_sum.write_formula("C9", "=SUM($C$4:$C$8)", format_table_titles) 202 | ws_sum.write_formula("D9", "=SUM($D$4:$D$8)", format_table_titles) 203 | ws_sum.write_formula("E9", "=SUM($E$4:$E$8)", format_table_titles) 204 | 205 | # -------------------- 206 | # CHART 207 | # -------------------- 208 | chart_vulns_summary = workbook.add_chart({'type': 'pie'}) 209 | chart_vulns_summary.add_series({ # type: ignore 210 | 'name': 'vulnerability summary by affected hosts', 211 | 'categories': '={}!B4:B8'.format(sheet_name), 212 | 'values': '={}!D4:D8'.format(sheet_name), 213 | 'data_labels': {'value': True, 'position': 'outside_end', 'leader_lines': True, 'font': {'name': 'Tahoma'}}, 214 | 'points': [ 215 | {'fill': {'color': Config.colors()['critical']}}, 216 | {'fill': {'color': Config.colors()['high']}}, 217 | {'fill': {'color': Config.colors()['medium']}}, 218 | {'fill': {'color': Config.colors()['low']}}, 219 | {'fill': {'color': Config.colors()['none']}}, 220 | ], 221 | }) 222 | chart_vulns_summary.set_title({'name': 'Vulnerability summary', 'overlay': False, 'name_font': {'name': 'Tahoma'}}) # type: ignore 223 | chart_vulns_summary.set_size({'width': 500, 'height': 300}) # type: ignore 224 | chart_vulns_summary.set_legend({'position': 'right', 'font': {'name': 'Tahoma'}}) # type: ignore 225 | ws_sum.insert_chart("G2", chart_vulns_summary) # type: ignore 226 | 227 | # -------------------- 228 | # VULN BY FAMILY 229 | # -------------------- 230 | ws_sum.merge_range("B19:C19", "VULNERABILITIES BY FAMILY", format_sheet_title_content) # type: ignore 231 | ws_sum.write("B20", "Family", format_table_titles) 232 | ws_sum.write("C20", "Vulnerabilities", format_table_titles) 233 | 234 | last = 21 235 | for i, (family, number) in enumerate(iter(vuln_by_family.items()), last): 236 | ws_sum.write("B{}".format(i), family, format_align_border) 237 | ws_sum.write("C{}".format(i), number, format_align_border) 238 | last = i 239 | 240 | ws_sum.write("B{}".format(str(last + 1)), "Total", format_table_titles) 241 | ws_sum.write_formula("C{}".format(str(last + 1)), "=SUM($C$21:$C${})".format(last), format_table_titles) 242 | 243 | # -------------------- 244 | # CHART 245 | # -------------------- 246 | chart_vulns_by_family = workbook.add_chart({'type': 'pie'}) 247 | chart_vulns_by_family.add_series({ # type: ignore 248 | 'name': 'vulnerability summary by family', 249 | 'categories': '={}!B21:B{}'.format(sheet_name, last), 250 | 'values': '={}!C21:C{}'.format(sheet_name, last), 251 | 'data_labels': {'value': True, 'position': 'best_fit', 'leader_lines': True, 'font': {'name': 'Tahoma'}}, 252 | }) 253 | chart_vulns_by_family.set_title({'name': 'Vulnerabilities by family', 'overlay': False, # type: ignore 254 | 'name_font': {'name': 'Tahoma'}}) 255 | chart_vulns_by_family.set_size({'width': 500, 'height': 500}) # type: ignore 256 | chart_vulns_by_family.set_legend({'position': 'bottom', 'font': {'name': 'Tahoma'}}) # type: ignore 257 | ws_sum.insert_chart("G19", chart_vulns_by_family) # type: ignore 258 | 259 | # ==================== 260 | # TABLE OF CONTENTS 261 | # ==================== 262 | sheet_name = "TOC" 263 | ws_toc = workbook.add_worksheet(sheet_name) 264 | ws_toc.set_tab_color(Config.colors()['blue']) 265 | 266 | ws_toc.set_column("A:A", 7) 267 | ws_toc.set_column("B:B", 5) 268 | ws_toc.set_column("C:C", 70) 269 | ws_toc.set_column("D:D", 15) 270 | ws_toc.set_column("E:E", 50) 271 | ws_toc.set_column("F:F", 7) 272 | 273 | ws_toc.merge_range("B2:E2", "TABLE OF CONTENTS", format_sheet_title_content) # type: ignore 274 | ws_toc.write("B3", "No.", format_table_titles) 275 | ws_toc.write("C3", "Vulnerability", format_table_titles) 276 | ws_toc.write("D3", "CVSS Score", format_table_titles) 277 | ws_toc.write("E3", "Hosts", format_table_titles) 278 | 279 | # ==================== 280 | # VULN SHEETS 281 | # ==================== 282 | for i, vuln in enumerate(vuln_info, 1): 283 | name = re.sub(r"[\[\]\\\'\"&@#():*?/]", "", vuln.name) 284 | if len(name) > 27: 285 | name = "{}..{}".format(name[0:15], name[-10:]) 286 | name = "{:03X}_{}".format(i, name) 287 | ws_vuln = workbook.add_worksheet(name) 288 | ws_vuln.set_tab_color(Config.colors()[vuln.level.lower()]) 289 | 290 | vuln.version = "" 291 | if(match := re.search(r'Installed version: ((\d|.)+)', vuln.hosts[0][1].result)): 292 | vuln.version = match.group(1) 293 | 294 | # -------------------- 295 | # TABLE OF CONTENTS 296 | # -------------------- 297 | ws_toc.write("B{}".format(i + 3), "{:03X}".format(i), format_table_cells) 298 | ws_toc.write_url("C{}".format(i + 3), "internal:'{}'!A1".format(name), format_table_cells, string=vuln.name) 299 | ws_toc.write("D{}".format(i + 3), "{:.1f} ({})".format(vuln.cvss, vuln.level.capitalize()), 300 | format_toc[vuln.level]) 301 | ws_toc.write("E{}".format(i + 3), "{}".format(', '.join([host.ip for host, _ in vuln.hosts])), 302 | format_table_cells) 303 | ws_vuln.write_url("A1", "internal:'{}'!A{}".format(ws_toc.get_name(), i + 3), format_align_center, 304 | string="<< TOC") 305 | ws_toc.set_row(i + 3, __row_height(name, 150), None) 306 | 307 | # -------------------- 308 | # VULN INFO 309 | # -------------------- 310 | ws_vuln.set_column("A:A", 7, format_align_center) 311 | ws_vuln.set_column("B:B", 20, format_align_center) 312 | ws_vuln.set_column("C:C", 20, format_align_center) 313 | ws_vuln.set_column("D:D", 50, format_align_center) 314 | ws_vuln.set_column("E:E", 15, format_align_center) 315 | ws_vuln.set_column("F:F", 15, format_align_center) 316 | ws_vuln.set_column("G:G", 20, format_align_center) 317 | ws_vuln.set_column("H:H", 7, format_align_center) 318 | content_width = 120 319 | 320 | ws_vuln.write('B2', "Title", format_table_titles) 321 | ws_vuln.merge_range("C2:G2", vuln.name, format_sheet_title_content) # type: ignore 322 | ws_vuln.set_row(1, __row_height(vuln.name, content_width), None) 323 | 324 | ws_vuln.write('B3', "Description", format_table_titles) 325 | ws_vuln.merge_range("C3:G3", vuln.description, format_table_cells) # type: ignore 326 | ws_vuln.set_row(2, __row_height(vuln.description, content_width), None) 327 | 328 | ws_vuln.write('B4', "Impact", format_table_titles) 329 | ws_vuln.merge_range("C4:G4", vuln.impact, format_table_cells) # type: ignore 330 | ws_vuln.set_row(3, __row_height(vuln.impact, content_width), None) 331 | 332 | ws_vuln.write('B5', "Recommendation", format_table_titles) 333 | ws_vuln.merge_range("C5:G5", vuln.solution, format_table_cells) # type: ignore 334 | ws_vuln.set_row(4, __row_height(vuln.solution, content_width), None) 335 | 336 | ws_vuln.write('B6', "Version", format_table_titles) 337 | ws_vuln.merge_range("C6:G6", vuln.version, format_table_cells) # type: ignore 338 | ws_vuln.set_row(5, __row_height(vuln.version, content_width), None) 339 | 340 | ws_vuln.write('B7', "Details", format_table_titles) 341 | ws_vuln.merge_range("C7:G7", vuln.insight, format_table_cells) # type: ignore 342 | ws_vuln.set_row(6, __row_height(vuln.insight, content_width), None) 343 | 344 | ws_vuln.write('B8', "CVEs", format_table_titles) 345 | cves = ", ".join(vuln.cves) 346 | cves = cves.upper() if cves != "" else "No CVE" 347 | ws_vuln.merge_range("C8:G8", cves, format_table_cells) # type: ignore 348 | ws_vuln.set_row(7, __row_height(cves, content_width), None) 349 | 350 | ws_vuln.write('B9', "CVSS", format_table_titles) 351 | cvss = float(vuln.cvss) 352 | if cvss >= 0.0: 353 | ws_vuln.merge_range("C9:G9", "{:.1f}".format(cvss), format_table_cells) # type: ignore 354 | else: 355 | ws_vuln.merge_range("C9:G9", "{}".format("No CVSS"), format_table_cells) # type: ignore 356 | 357 | ws_vuln.write('B10', "Level", format_table_titles) 358 | ws_vuln.merge_range("C10:G10", vuln.level.capitalize(), format_table_cells) # type: ignore 359 | 360 | ws_vuln.write('B11', "Family", format_table_titles) 361 | ws_vuln.merge_range("C11:G11", vuln.family, format_table_cells) # type: ignore 362 | 363 | ws_vuln.write('B12', "References", format_table_titles) 364 | ws_vuln.merge_range("C12:G12", " {}".format(vuln.references), format_table_cells) # type: ignore 365 | ws_vuln.set_row(10, __row_height(vuln.references, content_width), None) 366 | 367 | ws_vuln.write('C14', "IP", format_table_titles) 368 | ws_vuln.write('D14', "Host name", format_table_titles) 369 | ws_vuln.write('E14', "Port number", format_table_titles) 370 | ws_vuln.write('F14', "Port protocol", format_table_titles) 371 | ws_vuln.write('G14', "Result", format_table_titles) 372 | 373 | # -------------------- 374 | # AFFECTED HOSTS 375 | # -------------------- 376 | 377 | for j, (host, port) in enumerate(vuln.hosts, 15): 378 | 379 | ws_vuln.write("C{}".format(j), host.ip) 380 | ws_vuln.write("D{}".format(j), host.host_name if host.host_name else "-") 381 | 382 | if port: 383 | ws_vuln.write("E{}".format(j), "" if port.number == 0 else port.number) 384 | ws_vuln.write("F{}".format(j), port.protocol) 385 | ws_vuln.write("G{}".format(j), port.result, format_table_cells) 386 | ws_vuln.set_row(j, __row_height(port.result, content_width), None) 387 | else: 388 | ws_vuln.write("E{}".format(j), "No port info") 389 | 390 | workbook.close() 391 | 392 | 393 | def export_to_word_by_vuln(vuln_info:list[Vulnerability], template:str, output_file:str='openvas_report.docx'): 394 | """ 395 | Export vulnerabilities info in a Word file. 396 | 397 | :param vuln_info: Vulnerability list info 398 | :type vuln_info: list(Vulnerability) 399 | 400 | :param output_file: Filename of the Excel file 401 | :type output_file: str 402 | 403 | :param template: Path to Docx template 404 | :type template: str 405 | 406 | :raises: TypeError 407 | """ 408 | 409 | import matplotlib.pyplot as plt 410 | import numpy as np 411 | import tempfile 412 | import os 413 | 414 | from docx import Document 415 | from docx.oxml.shared import qn, OxmlElement 416 | from docx.oxml.ns import nsdecls 417 | from docx.oxml import parse_xml 418 | from docx.shared import Cm 419 | 420 | if not isinstance(vuln_info, list): 421 | raise TypeError("Expected list, got '{}' instead".format(type(vuln_info))) 422 | else: 423 | for x in vuln_info: 424 | if not isinstance(x, Vulnerability): 425 | raise TypeError("Expected Vulnerability, got '{}' instead".format(type(x))) 426 | if not isinstance(output_file, str): 427 | raise TypeError("Expected str, got '{}' instead".format(type(output_file))) 428 | else: 429 | if not output_file: 430 | raise ValueError("output_file must have a valid name.") 431 | if template is not None: 432 | if not isinstance(template, str): 433 | raise TypeError("Expected str, got '{}' instead".format(type(template))) 434 | else: 435 | # == HAMMER PROGRAMMING (beat it into submission) == 436 | # I had to use pkg_resources because I couldn't find this template any other way. 437 | import pkg_resources 438 | template = pkg_resources.resource_filename('openvasreporting', 'src/openvas-template.docx') 439 | 440 | #mpl_logger = logging.getLogger('matplotlib') 441 | #mpl_logger.setLevel(logging.NOTSET) 442 | 443 | vuln_info, vuln_levels, vuln_host_by_level, vuln_by_family = _get_collections(vuln_info) 444 | 445 | # ==================== 446 | # DOCUMENT PROPERTIES 447 | # ==================== 448 | document = Document(template) 449 | 450 | doc_prop = document.core_properties 451 | doc_prop.title = "OpenVAS Report" 452 | doc_prop.category = "Report" 453 | 454 | document.add_paragraph('OpenVAS Report', style='Title') 455 | 456 | # ==================== 457 | # TABLE OF CONTENTS 458 | # ==================== 459 | document.add_paragraph('Table of Contents', style='Heading 1') 460 | 461 | par = document.add_paragraph() 462 | run = par.add_run() 463 | fld_char = OxmlElement('w:fldChar') # creates a new element 464 | fld_char.set(qn('w:fldCharType'), 'begin') # sets attribute on element 465 | instr_text = OxmlElement('w:instrText') 466 | instr_text.set(qn('xml:space'), 'preserve') # sets attribute on element 467 | instr_text.text = r'TOC \h \z \t "OV-H1toc;1;OV-H2toc;2;OV-H3toc;3;OV-Finding;3"' # type: ignore 468 | 469 | fld_char2 = OxmlElement('w:fldChar') 470 | fld_char2.set(qn('w:fldCharType'), 'separate') 471 | fld_char3 = OxmlElement('w:t') 472 | fld_char3.text = "# Right-click to update field. #" # type: ignore 473 | fld_char2.append(fld_char3) 474 | 475 | fld_char4 = OxmlElement('w:fldChar') 476 | fld_char4.set(qn('w:fldCharType'), 'end') 477 | 478 | r_element = run._r 479 | r_element.append(fld_char) 480 | r_element.append(instr_text) 481 | r_element.append(fld_char2) 482 | r_element.append(fld_char4) 483 | 484 | document.add_page_break() 485 | 486 | # ==================== 487 | # MANAGEMENT SUMMARY 488 | # ==================== 489 | document.add_paragraph('Management Summary', style='OV-H1toc') 490 | document.add_paragraph('< TYPE YOUR MANAGEMENT SUMMARY HERE >') 491 | document.add_page_break() 492 | 493 | # ==================== 494 | # TECHNICAL FINDINGS 495 | # ==================== 496 | document.add_paragraph('Technical Findings', style='OV-H1toc') 497 | document.add_paragraph('The section below discusses the technical findings.') 498 | 499 | # -------------------- 500 | # SUMMARY TABLE 501 | # -------------------- 502 | document.add_paragraph('Summary', style='OV-H2toc') 503 | 504 | colors_sum = [] 505 | labels_sum = [] 506 | vuln_sum = [] 507 | aff_sum = [] 508 | 509 | table_summary = document.add_table(rows=1, cols=3) 510 | hdr_cells = table_summary.rows[0].cells 511 | hdr_cells[0].paragraphs[0].add_run('Risk level').bold = True 512 | hdr_cells[1].paragraphs[0].add_run('Vulns number').bold = True 513 | hdr_cells[2].paragraphs[0].add_run('Affected hosts').bold = True 514 | 515 | # Provide data to table and charts 516 | for level in Config.levels().values(): 517 | row_cells = table_summary.add_row().cells 518 | row_cells[0].text = level.capitalize() 519 | row_cells[1].text = str(vuln_levels[level]) 520 | row_cells[2].text = str(vuln_host_by_level[level]) 521 | colors_sum.append(Config.colors()[level]) 522 | labels_sum.append(level) 523 | vuln_sum.append(vuln_levels[level]) 524 | aff_sum.append(vuln_host_by_level[level]) 525 | 526 | # -------------------- 527 | # CHART 528 | # -------------------- 529 | fd, path = tempfile.mkstemp(suffix='.png') 530 | 531 | par_chart = document.add_paragraph() 532 | run_chart = par_chart.add_run() 533 | 534 | plt.figure() 535 | 536 | pos = np.arange(len(labels_sum)) 537 | width = 0.35 538 | 539 | bars_vuln = plt.bar(pos - width / 2, vuln_sum, width, align='center', label='Vulnerabilities', 540 | color=colors_sum, edgecolor='black') 541 | bars_aff = plt.bar(pos + width / 2, aff_sum, width, align='center', label='Affected hosts', 542 | color=colors_sum, edgecolor='black', hatch='//') 543 | plt.title('Vulnerability summary by risk level') 544 | plt.subplot().set_xticks(pos) 545 | plt.subplot().set_xticklabels(labels_sum) 546 | plt.gca().spines['left'].set_visible(False) 547 | plt.gca().spines['right'].set_visible(False) 548 | plt.gca().spines['top'].set_visible(False) 549 | plt.gca().spines['bottom'].set_position('zero') 550 | plt.tick_params(top=False, bottom=True, left=False, right=False, 551 | labelleft=False, labelbottom=True) 552 | plt.subplots_adjust(left=0.0, right=1.0) 553 | 554 | def __label_bars(barcontainer): 555 | for bar in barcontainer: 556 | height = bar.get_height() 557 | plt.gca().text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.3, str(int(height)), 558 | ha='center', color='black', fontsize=9) 559 | 560 | __label_bars(bars_vuln) 561 | __label_bars(bars_aff) 562 | 563 | plt.legend() 564 | 565 | plt.savefig(path) 566 | 567 | # plt.show() # DEBUG 568 | 569 | run_chart.add_picture(path, width=Cm(8.0)) 570 | 571 | plt.figure() 572 | 573 | values = list(vuln_by_family.values()) 574 | _, _, autotexts = plt.pie(values, labels=vuln_by_family.keys(), autopct='') # type: ignore 575 | plt.title('Vulnerability by family') 576 | for i, txt in enumerate(autotexts): 577 | txt.set_text('{}'.format(values[i])) 578 | plt.axis('equal') 579 | 580 | plt.savefig(path, bbox_inches='tight') # bbox_inches fixes labels being cut, however only on save not on show 581 | 582 | # plt.show() # DEBUG 583 | 584 | run_chart.add_picture(path, width=Cm(8.0)) 585 | os.close(fd) 586 | os.remove(path) 587 | 588 | # ==================== 589 | # VULN PAGES 590 | # ==================== 591 | cur_level = "" 592 | 593 | for i, vuln in enumerate(vuln_info, 1): 594 | # -------------------- 595 | # GENERAL 596 | # -------------------- 597 | level = vuln.level.lower() 598 | 599 | if level != cur_level: 600 | document.add_paragraph( 601 | level.capitalize(), style='OV-H2toc').paragraph_format.page_break_before = True 602 | cur_level = level 603 | else: 604 | document.add_page_break() 605 | 606 | title = "[{}] {}".format(level.upper(), vuln.name) 607 | document.add_paragraph(title, style='OV-Finding') 608 | 609 | table_vuln = document.add_table(rows=9, cols=3) 610 | table_vuln.autofit = False 611 | 612 | # COLOR 613 | # -------------------- 614 | col_cells = table_vuln.columns[0].cells 615 | col_cells[0].merge(col_cells[7]) 616 | color_fill = parse_xml(r''.format(nsdecls('w'), Config.colors()[vuln.level][1:])) 617 | col_cells[0]._tc.get_or_add_tcPr().append(color_fill) 618 | 619 | for col_cell in col_cells: 620 | col_cell.width = Cm(0.42) 621 | 622 | # TABLE HEADERS 623 | # -------------------- 624 | hdr_cells = table_vuln.columns[1].cells 625 | hdr_cells[0].paragraphs[0].add_run('Description').bold = True 626 | hdr_cells[1].paragraphs[0].add_run('Impact').bold = True 627 | hdr_cells[2].paragraphs[0].add_run('Recommendation').bold = True 628 | hdr_cells[3].paragraphs[0].add_run('Version').bold = True 629 | hdr_cells[4].paragraphs[0].add_run('Details').bold = True 630 | hdr_cells[5].paragraphs[0].add_run('CVSS').bold = True 631 | hdr_cells[6].paragraphs[0].add_run('CVEs').bold = True 632 | hdr_cells[7].paragraphs[0].add_run('Family').bold = True 633 | hdr_cells[8].paragraphs[0].add_run('References').bold = True 634 | 635 | for hdr_cell in hdr_cells: 636 | hdr_cell.width = Cm(3.58) 637 | 638 | # FIELDS 639 | # -------------------- 640 | cves = ", ".join(vuln.cves) 641 | cves = cves.upper() if cves != "" else "No CVE" 642 | 643 | cvss = str(vuln.cvss) if vuln.cvss != -1.0 else "No CVSS" 644 | 645 | vuln.version = "" 646 | if(match := re.search(r'Installed version: ((\d|.)+)', vuln.hosts[0][1].result)): 647 | vuln.version = match.group(1) 648 | 649 | txt_cells = table_vuln.columns[2].cells 650 | txt_cells[0].text = vuln.description 651 | txt_cells[1].text = vuln.impact 652 | txt_cells[2].text = vuln.solution 653 | txt_cells[3].text = vuln.version 654 | txt_cells[4].text = vuln.insight 655 | txt_cells[5].text = cvss 656 | txt_cells[6].text = cves 657 | txt_cells[7].text = vuln.family 658 | txt_cells[8].text = vuln.references 659 | 660 | for txt_cell in txt_cells: 661 | txt_cell.width = Cm(12.50) 662 | 663 | # VULN HOSTS 664 | # -------------------- 665 | document.add_paragraph('Vulnerable hosts', style='Heading 4') 666 | 667 | # add coloumn for result per port and resize columns 668 | table_hosts = document.add_table(cols=5, rows=(len(vuln.hosts) + 1)) 669 | 670 | col_cells = table_hosts.columns[1].cells 671 | for col_cell in col_cells: 672 | col_cell.width = Cm(3.2) 673 | 674 | col_cells = table_hosts.columns[2].cells 675 | for col_cell in col_cells: 676 | col_cell.width = Cm(3.2) 677 | 678 | col_cells = table_hosts.columns[2].cells 679 | for col_cell in col_cells: 680 | col_cell.width = Cm(1.6) 681 | 682 | col_cells = table_hosts.columns[3].cells 683 | for col_cell in col_cells: 684 | col_cell.width = Cm(1.6) 685 | 686 | col_cells = table_hosts.columns[4].cells 687 | for col_cell in col_cells: 688 | col_cell.width = Cm(6.4) 689 | 690 | hdr_cells = table_hosts.rows[0].cells 691 | hdr_cells[0].paragraphs[0].add_run('IP').bold = True 692 | hdr_cells[1].paragraphs[0].add_run('Host name').bold = True 693 | hdr_cells[2].paragraphs[0].add_run('Port number').bold = True 694 | hdr_cells[3].paragraphs[0].add_run('Port protocol').bold = True 695 | hdr_cells[4].paragraphs[0].add_run('Port result').bold = True 696 | 697 | for j, (host, port) in enumerate(vuln.hosts, 1): 698 | cells = table_hosts.rows[j].cells 699 | cells[0].text = host.ip 700 | cells[1].text = host.host_name if host.host_name else "-" 701 | if port and port is not None: 702 | cells[2].text = "-" if port.number == 0 else str(port.number) 703 | cells[3].text = port.protocol 704 | cells[4].text = port.result 705 | else: 706 | cells[2].text = "No port info" 707 | 708 | document.save(output_file) 709 | 710 | 711 | def export_to_csv_by_vuln(vuln_info, template=None, output_file:str='openvas_report.csv'): 712 | """ 713 | Export vulnerabilities info in a Comma Separated Values (csv) file 714 | 715 | :param vuln_info: Vulnerability list info 716 | :type vuln_info: list(Vulnerability) 717 | 718 | :param template: Not supported in csv-output 719 | :type template: NoneType 720 | 721 | :param output_file: Filename of the csv file 722 | :type output_file: str 723 | 724 | :raises: TypeError, NotImplementedError 725 | """ 726 | 727 | import csv 728 | 729 | if not isinstance(vuln_info, list): 730 | raise TypeError("Expected list, got '{}' instead".format(type(vuln_info))) 731 | else: 732 | for x in vuln_info: 733 | if not isinstance(x, Vulnerability): 734 | raise TypeError("Expected Vulnerability, got '{}' instead".format(type(x))) 735 | if not isinstance(output_file, str): 736 | raise TypeError("Expected str, got '{}' instead".format(type(output_file))) 737 | else: 738 | if not output_file: 739 | raise ValueError("output_file must have a valid name.") 740 | if template is not None: 741 | raise NotImplementedError("Use of template is not supported in CSV-output.") 742 | 743 | vuln_info, _, _, _ = _get_collections(vuln_info) 744 | 745 | with open(output_file, 'w') as csvfile: 746 | fieldnames = ['hostname', 'ip', 'port', 'protocol', 747 | 'vulnerability', 'cvss', 'threat', 'family', 748 | 'description', 'detection', 'insight', 'impact', 'affected', 'solution', 'solution_type', 749 | 'vuln_id', 'cve', 'references'] 750 | writer = csv.DictWriter(csvfile, dialect='excel', fieldnames=fieldnames) 751 | writer.writeheader() 752 | 753 | for vuln in vuln_info: 754 | for (host, port) in vuln.hosts: 755 | rowdata = { 756 | 'hostname': host.host_name, 757 | 'ip': host.ip, 758 | 'port': port.number, 759 | 'protocol': port.protocol, 760 | 'vulnerability': vuln.name, 761 | 'cvss': vuln.cvss, 762 | 'threat': vuln.level, 763 | 'family': vuln.family, 764 | 'description': vuln.description, 765 | 'detection': vuln.detect, 766 | 'insight': vuln.insight, 767 | 'impact': vuln.impact, 768 | 'affected': vuln.affected, 769 | 'solution': vuln.solution, 770 | 'solution_type': vuln.solution_type, 771 | 'vuln_id': vuln.vuln_id, 772 | 'cve': ' - '.join(vuln.cves), 773 | 'references': ' - '.join(vuln.references) 774 | } 775 | writer.writerow(rowdata) 776 | 777 | 778 | def export_to_excel_by_host(resulttree: ResultTree, template=None, output_file:str='openvas_report.xlsx'): 779 | """ 780 | Export vulnerabilities info in an Excel file. 781 | 782 | :param resulttree: Vulnerability list info 783 | :type resulttree: resulttree 784 | :param template: Not supported in xlsx-output 785 | :type template: NoneType 786 | 787 | :param output_file: Filename of the Excel file 788 | :type output_file: str 789 | 790 | :raises: TypeError, NotImplementedError 791 | """ 792 | 793 | import xlsxwriter 794 | 795 | if not isinstance(resulttree, ResultTree): 796 | raise TypeError("Expected ResultTree, got '{}' instead".format(type(resulttree))) 797 | else: 798 | for key in resulttree.keys(): 799 | if not isinstance(resulttree[key], Host): 800 | raise TypeError("Expected Host, got '{}' instead".format(type(resulttree[key]))) 801 | if not isinstance(output_file, str): 802 | raise TypeError("Expected str, got '{}' instead".format(type(output_file))) 803 | else: 804 | if not output_file: 805 | raise ValueError("output_file must have a valid name.") 806 | if template is not None: 807 | raise NotImplementedError("Use of template is not supported in XSLX-output.") 808 | 809 | # ==================== 810 | # FUNCTIONS 811 | # ==================== 812 | def __row_height(text, width): 813 | return (max((len(text) // width), text.count('\n')) + 1) * 15 814 | 815 | workbook = xlsxwriter.Workbook(output_file) 816 | 817 | workbook.set_properties({ 818 | 'title': output_file, 819 | 'subject': 'OpenVAS report', 820 | 'author': 'TheGroundZero, ecgf(IcatuHolding)', 821 | 'category': 'report', 822 | 'keywords': 'OpenVAS, report', 823 | 'comments': 'TheGroundZero (https://github.com/TheGroundZero)'}) 824 | 825 | # ==================== 826 | # FORMATTING 827 | # ==================== 828 | workbook.formats[0].set_font_name('Tahoma') 829 | 830 | format_sheet_title_content = workbook.add_format({'font_name': 'Tahoma', 'font_size': 12, 831 | 'font_color': Config.colors()['blue'], 'bold': True, 832 | 'align': 'center', 'valign': 'vcenter', 'border': 1}) 833 | format_table_titles = workbook.add_format({'font_name': 'Tahoma', 'font_size': 11, 834 | 'font_color': 'white', 'bold': True, 835 | 'align': 'center', 'valign': 'vcenter', 836 | 'border': 1, 837 | 'bg_color': Config.colors()['blue']}) 838 | format_table_left_item = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 839 | 'font_color': Config.colors()['blue'], 'bold': True, 840 | 'align': 'left', 'valign': 'vcenter', 'border': 1}) 841 | format_table_cells = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 842 | 'align': 'left', 'valign': 'top', 843 | 'border': 1, 'text_wrap': 1}) 844 | format_align_center = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 845 | 'align': 'center', 'valign': 'top'}) 846 | format_align_left = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 847 | 'align': 'left', 'valign': 'top'}) 848 | format_align_right = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 849 | 'align': 'right', 'valign': 'top'}) 850 | format_align_border_left = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 851 | 'align': 'left', 'valign': 'top', 852 | 'border': 1, 'text_wrap': 1}) 853 | format_align_border_right = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 854 | 'align': 'right', 'valign': 'top', 855 | 'border': 1, 'text_wrap': 1}) 856 | format_number_border_right = workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 857 | 'align': 'right', 'valign': 'top', 858 | 'border': 1, 'text_wrap': 1}) 859 | format_number_border_right.num_format = '#.00' 860 | format_toc = { 861 | 'critical': workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 'font_color': 'white', 862 | 'align': 'center', 'valign': 'top', 863 | 'border': 1, 864 | 'bg_color': Config.colors()['critical']}), 865 | 'high': workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 'font_color': 'white', 866 | 'align': 'center', 'valign': 'top', 867 | 'border': 1, 'bg_color': Config.colors()['high']}), 868 | 'medium': workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 'font_color': 'white', 869 | 'align': 'center', 'valign': 'top', 870 | 'border': 1, 'bg_color': Config.colors()['medium']}), 871 | 'low': workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 'font_color': 'white', 872 | 'align': 'center', 'valign': 'top', 873 | 'border': 1, 'bg_color': Config.colors()['low']}), 874 | 'none': workbook.add_format({'font_name': 'Tahoma', 'font_size': 10, 'font_color': 'white', 875 | 'align': 'center', 'valign': 'top', 876 | 'border': 1, 'bg_color': Config.colors()['none']}) 877 | } 878 | 879 | # ==================== 880 | # SUMMARY SHEET 881 | # ==================== 882 | sheet_name = "Summary" 883 | ws_sum = workbook.add_worksheet(sheet_name) 884 | ws_sum.set_tab_color(Config.colors()['blue']) 885 | 886 | ws_sum.set_column("A:A", 3, format_align_center) 887 | ws_sum.set_column("B:B", 8, format_align_left) 888 | ws_sum.set_column("C:C", 30, format_align_left) 889 | ws_sum.set_column("D:D", 15, format_align_right) # critical 890 | ws_sum.set_column("E:E", 8, format_align_right) # high 891 | ws_sum.set_column("F:F", 8, format_align_right) # medium 892 | ws_sum.set_column("G:G", 8, format_align_right) # low 893 | ws_sum.set_column("H:H", 8, format_align_right) # none 894 | ws_sum.set_column("I:I", 8, format_align_right) # total 895 | ws_sum.set_column("J:J", 8, format_align_right) # severity 896 | ws_sum.set_column("K:K", 7, format_align_center) 897 | 898 | # --------------------- 899 | # MAX 10 HOSTS 900 | # --------------------- 901 | if len(resulttree) < 10: 902 | max_hosts = len(resulttree) 903 | else: 904 | max_hosts = 10 905 | 906 | # -------------------------- 907 | # HOST SUM SEVERITY SUMMARY 908 | # -------------------------- 909 | ws_sum.merge_range("B2:J2", "Hosts Ranking", format_sheet_title_content) # type: ignore 910 | ws_sum.write("B3", "#", format_table_titles) 911 | ws_sum.write("C3", "Hostname", format_table_titles) 912 | ws_sum.write("D3", "IP", format_table_titles) 913 | ws_sum.write("E3", "critical", format_table_titles) 914 | ws_sum.write("F3", "high", format_table_titles) 915 | ws_sum.write("G3", "medium", format_table_titles) 916 | ws_sum.write("H3", "low", format_table_titles) 917 | ws_sum.write("I3", "total", format_table_titles) 918 | ws_sum.write("J3", "severity", format_table_titles) 919 | 920 | temp_resulttree = resulttree.sorted_keys_by_rank() 921 | 922 | for i, key in enumerate(temp_resulttree[:max_hosts], 4): 923 | ws_sum.write("B{}".format(i), i-3, format_table_left_item) 924 | ws_sum.write("C{}".format(i), resulttree[key].host_name, format_table_left_item) 925 | ws_sum.write("D{}".format(i), resulttree[key].ip, format_table_left_item) 926 | ws_sum.write("E{}".format(i), resulttree[key].nv['critical'], format_align_border_right) 927 | ws_sum.write("F{}".format(i), resulttree[key].nv['high'], format_align_border_right) 928 | ws_sum.write("G{}".format(i), resulttree[key].nv['medium'], format_align_border_right) 929 | ws_sum.write("H{}".format(i), resulttree[key].nv['low'], format_align_border_right) 930 | ws_sum.write("I{}".format(i), resulttree[key].nv_total(), format_align_border_right) 931 | ws_sum.write("J{}".format(i), resulttree[key].higher_cvss, 932 | format_toc.get(tmp if (tmp := Config.cvss_level(resulttree[key].higher_cvss)) else "")) 933 | 934 | # -------------------- 935 | # CHART 936 | # -------------------- 937 | chart_sumcvss_summary = workbook.add_chart({'type': 'column'}) 938 | 939 | if not chart_sumcvss_summary: 940 | raise ValueError(chart_sumcvss_summary) 941 | 942 | chart_sumcvss_summary.add_series({ 943 | 'name': 'critical', 944 | 'categories': '={}!D4:D{}'.format(sheet_name, max_hosts + 3), 945 | 'values': '={}!E4:E{}'.format(sheet_name, max_hosts + 3), 946 | 'fill': { 'width': 8, 'color': Config.colors()['critical']}, 947 | 'border': { 'color': Config.colors()['blue']}, 948 | }) 949 | chart_sumcvss_summary.add_series({ 950 | 'name': 'high', 951 | 'categories': '={}!D4:D{}'.format(sheet_name, max_hosts + 3), 952 | 'values': '={}!F4:F{}'.format(sheet_name, max_hosts + 3), 953 | 'fill': { 'width': 8, 'color': Config.colors()['high']}, 954 | 'border': { 'color': Config.colors()['blue']}, 955 | }) 956 | chart_sumcvss_summary.add_series({ 957 | 'name': 'medium', 958 | 'categories': '={}!D4:D{}'.format(sheet_name, max_hosts + 3), 959 | 'values': '={}!G4:G{}'.format(sheet_name, max_hosts + 3), 960 | 'fill': { 'width': 8, 'color': Config.colors()['medium']}, 961 | 'border': { 'color': Config.colors()['blue']}, 962 | }) 963 | 964 | chart_sumcvss_summary.set_title({'name': 'Hosts by CVSS', 'overlay': False, 'font': {'name': 'Tahoma'}}) 965 | chart_sumcvss_summary.set_size({'width': 750, 'height': 350}) 966 | chart_sumcvss_summary.set_legend({'position': 'left', 'font': {'name': 'Tahoma'}}) 967 | chart_sumcvss_summary.set_x_axis({'label_position': 'bottom'}) 968 | chart_sumcvss_summary.set_x_axis({'num_font': {'name': 'Tahoma', 'size': 8}}) 969 | ws_sum.insert_chart(14, 1, chart_sumcvss_summary) 970 | 971 | # ==================== 972 | # TABLE OF CONTENTS 973 | # ==================== 974 | sheet_name = "TOC" 975 | ws_toc = workbook.add_worksheet(sheet_name) 976 | ws_toc.set_tab_color(Config.colors()['blue']) 977 | 978 | ws_toc.set_column("A:A", 3, format_align_center) 979 | ws_toc.set_column("B:B", 8, format_align_left) 980 | ws_toc.set_column("C:C", 30, format_align_left) 981 | ws_toc.set_column("D:D", 15, format_align_right) # critical 982 | ws_toc.set_column("E:E", 8, format_align_right) # high 983 | ws_toc.set_column("F:F", 8, format_align_right) # medium 984 | ws_toc.set_column("G:G", 8, format_align_right) # low 985 | ws_toc.set_column("H:H", 8, format_align_right) # none 986 | ws_toc.set_column("I:I", 8, format_align_right) # total 987 | ws_toc.set_column("J:J", 8, format_align_right) # severity 988 | ws_toc.set_column("K:K", 7, format_align_center) 989 | 990 | # -------------------------- 991 | # HOST SUM SEVERITY SUMMARY 992 | # -------------------------- 993 | ws_toc.merge_range("B2:J2", "Hosts Ranking", format_sheet_title_content) # type: ignore 994 | ws_toc.write("B3", "#", format_table_titles) 995 | ws_toc.write("C3", "Hostname", format_table_titles) 996 | ws_toc.write("D3", "IP", format_table_titles) 997 | ws_toc.write("E3", "critical", format_table_titles) 998 | ws_toc.write("F3", "high", format_table_titles) 999 | ws_toc.write("G3", "medium", format_table_titles) 1000 | ws_toc.write("H3", "low", format_table_titles) 1001 | ws_toc.write("I3", "total", format_table_titles) 1002 | ws_toc.write("J3", "severity", format_table_titles) 1003 | 1004 | # ==================== 1005 | # HOST SHEETS 1006 | # ==================== 1007 | for i, key in enumerate(temp_resulttree, 1): 1008 | 1009 | # this host has any vulnerability whose cvss severity >= min_level? 1010 | if len(resulttree[key].vuln_list) == 0: 1011 | continue 1012 | 1013 | name = "{:03X} - {}".format(i, resulttree[key].ip) 1014 | ws_host = workbook.add_worksheet(name) 1015 | ws_host.set_tab_color(Config.cvss_color(resulttree[key].higher_cvss)) 1016 | ws_host.write_url("A1", "internal:'{}'!A{}".format(ws_toc.get_name(), i + 3), format_align_center, 1017 | string="<< TOC") 1018 | 1019 | # -------------------- 1020 | # TABLE OF CONTENTS 1021 | # -------------------- 1022 | ws_toc.write("B{}".format(i + 3), "{:03X}".format(i), format_table_cells) 1023 | ws_toc.write_url("C{}".format(i + 3), "internal:'{}'!A1".format(name), format_table_cells, 1024 | string=resulttree[key].host_name) 1025 | ws_toc.write("D{}".format(i+3), resulttree[key].ip, format_align_border_left) 1026 | ws_toc.write("E{}".format(i+3), resulttree[key].nv['critical'], format_align_border_right) 1027 | ws_toc.write("F{}".format(i+3), resulttree[key].nv['high'], format_align_border_right) 1028 | ws_toc.write("G{}".format(i+3), resulttree[key].nv['medium'], format_align_border_right) 1029 | ws_toc.write("H{}".format(i+3), resulttree[key].nv['low'], format_align_border_right) 1030 | ws_toc.write("I{}".format(i+3), resulttree[key].nv_total(), format_align_border_right) 1031 | ws_toc.write("J{}".format(i+3), resulttree[key].higher_cvss, 1032 | format_toc.get(tmp if (tmp := Config.cvss_level(resulttree[key].higher_cvss)) else "")) 1033 | ws_toc.set_row(i + 3, __row_height(name, 150), None) 1034 | 1035 | # -------------------- 1036 | # HOST VULN LIST 1037 | # -------------------- 1038 | ws_host.set_column("A:A", 7, format_align_center) 1039 | ws_host.set_column("B:B", 12, format_align_center) # cvss - (level) 1040 | ws_host.set_column("C:C", 22, format_align_center) # name 1041 | ws_host.set_column("D:D", 22, format_align_center) # version 1042 | ws_host.set_column("E:E", 22, format_align_center) # oid 1043 | ws_host.set_column("F:F", 10, format_align_center) # port.port/port.num 1044 | ws_host.set_column("G:G", 22, format_align_center) # family 1045 | ws_host.set_column("H:H", 22, format_align_center) # description 1046 | ws_host.set_column("I:I", 12, format_align_center) # recomendation (solution) 1047 | ws_host.set_column("J:J", 12, format_align_center) # recomendation type (solution_type) 1048 | ws_host.set_column("K:K", 7, format_align_center) 1049 | 1050 | ws_host.merge_range("B2:K2", resulttree[key].ip + ' - ' + resulttree[key].host_name, format_sheet_title_content) # type: ignore 1051 | ws_host.write('B3', "CVSS", format_table_titles) 1052 | ws_host.write('C3', "Name", format_table_titles) 1053 | ws_host.write('D3', "Version", format_table_titles) 1054 | ws_host.write('E3', "oid", format_table_titles) 1055 | ws_host.write('F3', "Port", format_table_titles) 1056 | ws_host.write('G3', "Family", format_table_titles) 1057 | ws_host.write('H3', "Description", format_table_titles) 1058 | ws_host.write('I3', "Recomendation", format_table_titles) 1059 | ws_host.write('J3', "Type of fix", format_table_titles) 1060 | 1061 | 1062 | for j, vuln in enumerate(resulttree[key].vuln_list, 4): 1063 | ws_host.write('B{}'.format(j), "{:.2f} ({})".format(vuln.cvss, vuln.level), 1064 | format_toc[vuln.level]) 1065 | ws_host.write('C{}'.format(j), vuln.name, format_align_border_left) 1066 | ws_host.write('D{}'.format(j), vuln.version, format_align_border_left) 1067 | ws_host.write('E{}'.format(j), vuln.vuln_id, format_align_border_left) 1068 | port = vuln.hosts[0][1] 1069 | if port is None or port.number == 0: 1070 | portnum = 'general' 1071 | else: 1072 | portnum = str(port.number) 1073 | ws_host.write('F{}'.format(j), portnum + '/' + port.protocol, format_align_border_left) 1074 | ws_host.write('G{}'.format(j), vuln.family, format_align_border_left) 1075 | ws_host.write('H{}'.format(j), vuln.description.replace('\n', ' '), format_align_border_left) 1076 | ws_host.write('I{}'.format(j), vuln.solution.replace('\n', ' '), format_align_border_left) 1077 | ws_host.write('J{}'.format(j), vuln.solution_type, format_align_border_left) 1078 | max_len = max(len(vuln.name), len(vuln.description), len(vuln.solution)) 1079 | ws_host.set_row(j-1, (int(max_len/30)+1)*15) 1080 | 1081 | workbook.close() 1082 | 1083 | 1084 | def export_to_csv_by_host(resulttree, template=None, output_file:str='openvas_report.csv'): 1085 | """ 1086 | Export vulnerabilities info in a Comma Separated Values (csv) file 1087 | 1088 | :param vuln_info: Vulnerability list info 1089 | :type vuln_info: list(Vulnerability) 1090 | 1091 | :param template: Not supported in csv-output 1092 | :type template: NoneType 1093 | 1094 | :param output_file: Filename of the csv file 1095 | :type output_file: str 1096 | 1097 | :raises: TypeError, NotImplementedError 1098 | """ 1099 | 1100 | import csv 1101 | 1102 | if not isinstance(resulttree, ResultTree): 1103 | raise TypeError("Expected ResultTree, got '{}' instead".format(type(resulttree))) 1104 | else: 1105 | for x in resulttree.values(): 1106 | if not isinstance(x, Host): 1107 | raise TypeError("Expected Vulnerability, got '{}' instead".format(type(x))) 1108 | if not isinstance(output_file, str): 1109 | raise TypeError("Expected str, got '{}' instead".format(type(output_file))) 1110 | else: 1111 | if not output_file: 1112 | raise ValueError("output_file must have a valid name.") 1113 | if template is not None: 1114 | raise NotImplementedError("Use of template is not supported in CSV-output.") 1115 | 1116 | sortedresults = resulttree.sortedbysumcvss() 1117 | 1118 | with open(output_file, 'w') as csvfile: 1119 | fieldnames = ['hostname', 'ip', 'port', 'protocol', 1120 | 'vulnerability', 'cvss', 'threat', 'family', 1121 | 'description', 'detection', 'version', 'insight', 'impact', 'affected', 'solution', 'solution_type', 1122 | 'vuln_id', 'cve', 'references'] 1123 | writer = csv.DictWriter(csvfile, dialect='excel', fieldnames=fieldnames) 1124 | writer.writeheader() 1125 | 1126 | for key in sortedresults: 1127 | for vuln in resulttree[key].vuln_list: 1128 | rowdata = { 1129 | 'hostname': resulttree[key].host_name, 1130 | 'ip': resulttree[key].ip, 1131 | 'port': vuln.hosts[0][1].number, 1132 | 'protocol': vuln.hosts[0][1].protocol, 1133 | 'vulnerability': vuln.name, 1134 | 'cvss': vuln.cvss, 1135 | 'threat': vuln.level, 1136 | 'family': vuln.family, 1137 | 'description': vuln.description, 1138 | 'detection': vuln.detect, 1139 | 'version': vuln.version, 1140 | 'insight': vuln.insight, 1141 | 'impact': vuln.impact, 1142 | 'affected': vuln.affected, 1143 | 'solution': vuln.solution, 1144 | 'solution_type': vuln.solution_type, 1145 | 'vuln_id': vuln.vuln_id, 1146 | 'cve': ' - '.join(vuln.cves), 1147 | 'references': ' - '.join(vuln.references) if isinstance(vuln.references, list) else vuln.references 1148 | } 1149 | writer.writerow(rowdata) 1150 | 1151 | 1152 | def export_summary_to_csv( 1153 | vuln_info, 1154 | template=None, 1155 | output_file='openvas_summary_report.csv' 1156 | ): 1157 | """ 1158 | Export summary info in a Comma Separated Values (csv) file 1159 | 1160 | :param vuln_info: Vulnerability list info 1161 | :type vuln_info: list(Vulnerability) 1162 | 1163 | :param template: Not supported in csv-output 1164 | :type template: NoneType 1165 | 1166 | :param output_file: Filename of the csv file 1167 | :type output_file: str 1168 | 1169 | :raises: TypeError, NotImplementedError 1170 | """ 1171 | 1172 | import csv 1173 | 1174 | if not isinstance(vuln_info, list): 1175 | raise TypeError("Expected list, got '{}' instead".format(type(vuln_info))) 1176 | else: 1177 | for x in vuln_info: 1178 | if not isinstance(x, Vulnerability): 1179 | raise TypeError("Expected Vulnerability, got '{}' instead".format(type(x))) 1180 | if not isinstance(output_file, str): 1181 | raise TypeError("Expected str, got '{}' instead".format(type(output_file))) 1182 | else: 1183 | if not output_file: 1184 | raise ValueError("output_file must have a valid name.") 1185 | if template is not None: 1186 | raise NotImplementedError("Use of template is not supported in CSV-output.") 1187 | 1188 | vuln_info, vuln_levels, vuln_host_by_level, _ = _get_collections(vuln_info) 1189 | 1190 | with open(output_file, 'w') as csvfile: 1191 | fieldnames = ['level', 'count', 'host_count'] 1192 | writer = csv.DictWriter(csvfile, dialect='excel', fieldnames=fieldnames) 1193 | writer.writeheader() 1194 | 1195 | for _, level in enumerate(Config.levels().values(), 4): 1196 | rowdata = { 1197 | 'level': level, 1198 | 'count': vuln_levels[level], 1199 | 'host_count': vuln_host_by_level[level] 1200 | } 1201 | writer.writerow(rowdata) 1202 | --------------------------------------------------------------------------------