├── nessus_file_reader
├── file
│ ├── __init__.py
│ └── file.py
├── host
│ ├── __init__.py
│ └── host.py
├── plugin
│ ├── __init__.py
│ └── plugin.py
├── scan
│ ├── __init__.py
│ └── scan.py
├── _version.py
├── __init__.py
├── __about__.py
├── __main__.py
└── utilities.py
├── requirements.txt
├── .gitignore
├── .vscode
├── settings.json
└── tasks.json
├── examples
├── nfr-plugin-example-2.py
├── nfr-plugin-example-3.py
├── nfr-file-example.py
├── nfr-plugin-example.py
├── nfr-scan-example.py
├── nfr-host-example.py
└── nfr_example_script.py
├── docs
├── index.rst
├── Makefile
├── make.bat
└── conf.py
├── .github
└── workflows
│ ├── python-publish.yml
│ └── python-package.yml
├── setup.py
├── CHANGELOG.md
├── README.md
└── LICENSE
/nessus_file_reader/file/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nessus_file_reader/host/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nessus_file_reader/plugin/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nessus_file_reader/scan/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nessus_file_reader/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.7.1"
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | click>=8.2.1
2 | tabulate>=0.9.0
3 | jmespath>=1.0.1
4 | requests>=2.32.5
5 | packaging>=25.0
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.nessus
2 | *.xlsx
3 | *.spec
4 | .idea/
5 | __pycache__
6 | build
7 | dist
8 | nessus_file_reader.egg-info
9 | test_files
10 | _build
11 | .DS_Store
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "virtualenvPath": "~/.virtualenvs/nfr/bin",
3 | "restructuredtext.confPath": "",
4 | "python.pythonPath": "~/.virtualenvs/nfr/bin/python"
5 | }
--------------------------------------------------------------------------------
/examples/nfr-plugin-example-2.py:
--------------------------------------------------------------------------------
1 | import nessus_file_reader as nfr
2 |
3 | nessus_scan_file = "./your_nessus_file.nessus"
4 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
5 |
6 | for report_host in nfr.scan.report_hosts(root):
7 | pido_19506 = nfr.plugin.plugin_output(root, report_host, "19506")
8 | print(f"Nessus Scan Information Plugin Output:\n{pido_19506}")
9 |
--------------------------------------------------------------------------------
/examples/nfr-plugin-example-3.py:
--------------------------------------------------------------------------------
1 | import nessus_file_reader as nfr
2 |
3 | nessus_scan_file = "./your_nessus_file.nessus"
4 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
5 |
6 | for report_host in nfr.scan.report_hosts(root):
7 | pidos_14272 = nfr.plugin.plugin_outputs(root, report_host, "14272")
8 | print(f"All findings for Netstat Portscanner (SSH): \n{pidos_14272}")
9 |
--------------------------------------------------------------------------------
/examples/nfr-file-example.py:
--------------------------------------------------------------------------------
1 | import nessus_file_reader as nfr
2 |
3 | nessus_scan_file = "./your_nessus_file.nessus"
4 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
5 | file_name = nfr.file.nessus_scan_file_name_with_path(nessus_scan_file)
6 | file_size = nfr.file.nessus_scan_file_size_human(nessus_scan_file)
7 | print(f"File name: {file_name}")
8 | print(f"File size: {file_size}")
9 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. nessus-file-reader documentation master file, created by
2 | sphinx-quickstart on Sat Jul 25 19:06:04 2020.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | nessus-file-reader's documentation
7 | ==================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 |
14 |
15 | Indices and tables
16 | ==================
17 |
18 | * :ref:`genindex`
19 | * :ref:`modindex`
20 | * :ref:`search`
21 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/nessus_file_reader/__init__.py:
--------------------------------------------------------------------------------
1 | from .__about__ import (
2 | __title__,
3 | __icon__,
4 | __summary__,
5 | __uri__,
6 | __version__,
7 | __release_date__,
8 | __author__,
9 | __email__,
10 | __license_name__,
11 | __license_link__,
12 | __copyright__,
13 | )
14 |
15 | __all__ = [
16 | "__title__",
17 | "__icon__",
18 | "__summary__",
19 | "__uri__",
20 | "__version__",
21 | "__release_date__",
22 | "__author__",
23 | "__email__",
24 | "__license_name__",
25 | "__license_link__",
26 | "__copyright__",
27 | ]
28 |
29 | from .file import file
30 | from .host import host
31 | from .plugin import plugin
32 | from .scan import scan
33 |
34 | name = "nessus_file_reader"
35 |
--------------------------------------------------------------------------------
/examples/nfr-plugin-example.py:
--------------------------------------------------------------------------------
1 | import nessus_file_reader as nfr
2 |
3 | nessus_scan_file = "./your_nessus_file.nessus"
4 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
5 |
6 | for report_host in nfr.scan.report_hosts(root):
7 | report_items_per_host = nfr.host.report_items(report_host)
8 | for report_item in report_items_per_host:
9 | plugin_id = int(nfr.plugin.report_item_value(report_item, "pluginID"))
10 | risk_factor = nfr.plugin.report_item_value(report_item, "risk_factor")
11 | see_also = nfr.plugin.report_item_value(report_item, "see_also")
12 | description = nfr.plugin.report_item_value(report_item, "description")
13 | plugin_name = nfr.plugin.report_item_value(report_item, "pluginName")
14 | print("\t", plugin_id, " \t\t\t", risk_factor, " \t\t\t", plugin_name)
15 | print(see_also)
16 | print(description)
17 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/examples/nfr-scan-example.py:
--------------------------------------------------------------------------------
1 | import nessus_file_reader as nfr
2 |
3 | nessus_scan_file = "./your_nessus_file.nessus"
4 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
5 |
6 | report_name = nfr.scan.report_name(root)
7 | number_of_target_hosts = nfr.scan.number_of_target_hosts(root)
8 | number_of_scanned_hosts = nfr.scan.number_of_scanned_hosts(root)
9 | number_of_scanned_hosts_with_credentialed_checks_yes = (
10 | nfr.scan.number_of_scanned_hosts_with_credentialed_checks_yes(root)
11 | )
12 | scan_time_start = nfr.scan.scan_time_start(root)
13 | scan_time_end = nfr.scan.scan_time_end(root)
14 | scan_time_elapsed = nfr.scan.scan_time_elapsed(root)
15 | print(f" Report name: {report_name}")
16 | print(
17 | f" Number of target/scanned/credentialed hosts: {number_of_target_hosts}/{number_of_scanned_hosts}/{number_of_scanned_hosts_with_credentialed_checks_yes}"
18 | )
19 | print(
20 | f" Scan time START - END (ELAPSED): {scan_time_start} - {scan_time_end} ({scan_time_elapsed})"
21 | )
22 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | release:
13 | types: [published]
14 |
15 | jobs:
16 | deploy:
17 |
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 | - name: Set up Python
23 | uses: actions/setup-python@v5
24 | with:
25 | python-version: '3.x'
26 | - name: Install dependencies
27 | run: |
28 | python -m pip install --upgrade pip
29 | pip install build setuptools wheel twine
30 | - name: Build package
31 | run: python setup.py sdist bdist_wheel
32 | - name: Publish package
33 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
34 | with:
35 | user: __token__
36 | password: ${{ secrets.PYPI_API_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "Black Check",
6 | "type": "shell",
7 | "command": "${config:virtualenvPath}/black . --check"
8 | },
9 | {
10 | "label": "Black Fix",
11 | "type": "shell",
12 | "command": "${config:virtualenvPath}/black ."
13 | },
14 | {
15 | "label": "Install Dependencies",
16 | "type": "shell",
17 | "command": "${config:virtualenvPath}/pip install -r requirements.txt"
18 | },
19 | {
20 | "label": "Build Application",
21 | "type": "shell",
22 | "command": "${config:virtualenvPath}/python setup.py sdist bdist_wheel"
23 | },
24 | {
25 | "label": "Start sphinx-autobuild",
26 | "type": "shell",
27 | "options": {
28 | "env": {
29 | "PROJECT_PORT": "8061"
30 | }
31 | },
32 | "command": "source ${config:virtualenvPath}/activate; sphinx-autobuild ${workspaceFolder}/docs ${workspaceFolder}/docs/_build/html -a --port $PROJECT_PORT --open-browser"
33 | },
34 | {
35 | "label": "Upgrade pip",
36 | "type": "shell",
37 | "command": "${config:virtualenvPath}/pip install --upgrade pip"
38 | },
39 | ]
40 | }
--------------------------------------------------------------------------------
/.github/workflows/python-package.yml:
--------------------------------------------------------------------------------
1 | name: Python package
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 | workflow_dispatch:
8 |
9 | env:
10 | python_package_name: nessus-file-reader
11 | folder_package_name: nessus_file_reader
12 |
13 | jobs:
14 | build:
15 |
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | max-parallel: 1
19 | matrix:
20 | os: [ubuntu-latest, windows-latest, macos-latest]
21 | python-version: ['3.10', '3.11', '3.12', '3.13']
22 |
23 | steps:
24 | - uses: actions/checkout@v4
25 | - name: Set up Python ${{ matrix.python-version }}
26 | uses: actions/setup-python@v5
27 | with:
28 | python-version: ${{ matrix.python-version }}
29 | - name: Install tools
30 | run: |
31 | python -m pip install --upgrade pip build setuptools wheel twine
32 | - name: Install dependencies
33 | run: |
34 | pip install -r requirements.txt
35 | - name: Build package
36 | run: python setup.py sdist bdist_wheel
37 |
38 | - name: Install locally
39 | run: |
40 | ls
41 | ls dist
42 | TOOL_CURRENT_VERSION=`sed -e 's/.*__version__ = "\(.*\)".*/\1/' ${{ env.folder_package_name }}/_version.py`
43 | pip install dist/${{ env.folder_package_name }}-${TOOL_CURRENT_VERSION}-py3-none-any.whl
44 | shell: bash
45 | - name: pip show package
46 | run: |
47 | pip show ${{ env.python_package_name }}
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | with open("requirements.txt") as f:
7 | required = f.read().splitlines()
8 |
9 | about = {}
10 | with open("nessus_file_reader/_version.py") as f:
11 | exec(f.read(), about)
12 |
13 | setuptools.setup(
14 | name="nessus_file_reader",
15 | version=about["__version__"],
16 | license="GPLv3",
17 | author="Damian Krawczyk",
18 | author_email="damian.krawczyk@limberduck.org",
19 | description="nessus file reader (NFR) by LimberDuck is a CLI tool and python module "
20 | "created to quickly parse nessus files containing the results of scans "
21 | "performed by using Nessus by (C) Tenable, Inc.",
22 | long_description=long_description,
23 | long_description_content_type="text/markdown",
24 | url="https://github.com/LimberDuck/nessus-file-reader",
25 | packages=setuptools.find_packages(),
26 | install_requires=required,
27 | entry_points={"console_scripts": ["nfr = nessus_file_reader.__main__:main"]},
28 | classifiers=[
29 | "Programming Language :: Python :: 3.13",
30 | "Programming Language :: Python :: 3.12",
31 | "Programming Language :: Python :: 3.11",
32 | "Programming Language :: Python :: 3.10",
33 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
34 | "Operating System :: OS Independent",
35 | "Development Status :: 4 - Beta",
36 | "Environment :: Console",
37 | ],
38 | )
39 |
--------------------------------------------------------------------------------
/examples/nfr-host-example.py:
--------------------------------------------------------------------------------
1 | import nessus_file_reader as nfr
2 |
3 | nessus_scan_file = "./your_nessus_file.nessus"
4 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
5 |
6 | for report_host in nfr.scan.report_hosts(root):
7 | report_host_name = nfr.host.report_host_name(report_host)
8 | report_host_os = nfr.host.detected_os(report_host)
9 | report_host_scan_time_start = nfr.host.host_time_start(report_host)
10 | report_host_scan_time_end = nfr.host.host_time_end(report_host)
11 | report_host_scan_time_elapsed = nfr.host.host_time_elapsed(report_host)
12 | report_host_critical = nfr.host.number_of_plugins_per_risk_factor(
13 | report_host, "Critical"
14 | )
15 | report_host_high = nfr.host.number_of_plugins_per_risk_factor(report_host, "High")
16 | report_host_medium = nfr.host.number_of_plugins_per_risk_factor(
17 | report_host, "Medium"
18 | )
19 | report_host_low = nfr.host.number_of_plugins_per_risk_factor(report_host, "Low")
20 | report_host_none = nfr.host.number_of_plugins_per_risk_factor(report_host, "None")
21 | print(f" Report host name: {report_host_name}")
22 | print(f" Report host OS: {report_host_os}")
23 | print(
24 | f" Host scan time START - END (ELAPSED): {report_host_scan_time_start} - {report_host_scan_time_end} ({report_host_scan_time_elapsed})"
25 | )
26 | print(
27 | f" Critical/High/Medium/Low/None findings: {report_host_critical}/{report_host_high}/{report_host_medium}/{report_host_low}/{report_host_none}"
28 | )
29 |
--------------------------------------------------------------------------------
/nessus_file_reader/__about__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | nessus file reader (NFR) by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a python module
4 | created to quickly parse nessus files containing the results of scans
5 | performed by using Nessus by (C) Tenable, Inc.
6 | Copyright (C) 2019 Damian Krawczyk
7 |
8 | This program is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation, either version 3 of the License, or
11 | (at your option) any later version.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 | """
21 |
22 | __all__ = [
23 | "__title__",
24 | "__icon__",
25 | "__summary__",
26 | "__uri__",
27 | "__version__",
28 | "__release_date__",
29 | "__author__",
30 | "__email__",
31 | "__license_name__",
32 | "__license_link__",
33 | "__copyright__",
34 | ]
35 |
36 | __title__ = "nessus file reader (NFR) by LimberDuck"
37 | __package_name__ = "nessus-file-reader"
38 | __icon__ = "LimberDuck-nessus-file-reader.ico"
39 | __summary__ = (
40 | "nessus file reader (NFR) by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a CLI tool and python module"
41 | "created to quickly parse nessus files containing the results of scans"
42 | "performed by using Nessus by (C) Tenable, Inc."
43 | )
44 | __uri__ = "https://github.com/LimberDuck"
45 | __version__ = "0.7.1"
46 | __release_date__ = "2025.09.01"
47 | __author__ = "Damian Krawczyk"
48 | __email__ = "damian.krawczyk@limberduck.org"
49 | __license_name__ = "GNU GPLv3"
50 | __license_link__ = "https://www.gnu.org/licenses/gpl-3.0.en.html"
51 | __copyright__ = "\N{COPYRIGHT SIGN} 2019-2025 by %s" % __author__
52 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | # import os
14 | # import sys
15 | # sys.path.insert(0, os.path.abspath('.'))
16 |
17 |
18 | # -- Project information -----------------------------------------------------
19 |
20 | project = 'nessus-file-reader'
21 | copyright = '2018-2025, Damian Krawczyk'
22 | author = 'Damian Krawczyk'
23 |
24 | # The full version, including alpha/beta/rc tags
25 | release = '0.3.0'
26 |
27 |
28 | # -- General configuration ---------------------------------------------------
29 |
30 | # Add any Sphinx extension module names here, as strings. They can be
31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
32 | # ones.
33 | extensions = [
34 | "sphinx_rtd_theme",
35 | ]
36 |
37 | # Add any paths that contain templates here, relative to this directory.
38 | templates_path = ['_templates']
39 |
40 | # List of patterns, relative to source directory, that match files and
41 | # directories to ignore when looking for source files.
42 | # This pattern also affects html_static_path and html_extra_path.
43 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
44 |
45 |
46 | # -- Options for HTML output -------------------------------------------------
47 |
48 | # The theme to use for HTML and HTML Help pages. See the documentation for
49 | # a list of builtin themes.
50 | #
51 | # html_theme = 'alabaster'
52 | html_theme = 'sphinx_rtd_theme'
53 |
54 | # Add any paths that contain custom static files (such as style sheets) here,
55 | # relative to this directory. They are copied after the builtin static files,
56 | # so a file named "default.css" will overwrite the builtin "default.css".
57 | html_static_path = ['_static']
58 |
59 | master_doc = 'index'
60 |
--------------------------------------------------------------------------------
/nessus_file_reader/file/file.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | nessus file reader (NFR) by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a python module
4 | created to quickly parse nessus files containing the results of scans
5 | performed by using Nessus by (C) Tenable, Inc.
6 | Copyright (C) 2019 Damian Krawczyk
7 |
8 | This program is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation, either version 3 of the License, or
11 | (at your option) any later version.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 | """
21 |
22 | import os
23 | from xml.etree.ElementTree import parse
24 |
25 |
26 | def nessus_scan_file_name_with_path(file):
27 | """
28 | Function returns a normalized absolute version of the path.
29 | :param file: given nessus file
30 | :return: normalized absolute version of the given file path.
31 | """
32 | nessus_scan_file_name = os.path.abspath(file)
33 | return nessus_scan_file_name
34 |
35 |
36 | def nessus_scan_file_size(file):
37 | """
38 | Function returns the size in bytes of path.
39 | :param file: given nessus file
40 | :return: size in bytes of path.
41 | """
42 | file_size = os.path.getsize(file)
43 | return file_size
44 |
45 |
46 | def nessus_scan_file_size_human(file):
47 | """
48 | Function convert nessus file size from bytes to size more convenient to read by human.
49 | :param file: given nessus file
50 | :return: size in human readable form
51 | """
52 | size = nessus_scan_file_size(file)
53 | suffix = "B"
54 | for unit in [" b", " Ki", " Mi", " Gi", " Ti", " Pi", " Ei", " Zi"]:
55 | if abs(size) < 1024.0:
56 | return "%3.1f%s%s" % (size, unit, suffix)
57 | size /= 1024.0
58 | return "%.1f%s%s" % (size, "Yi", suffix)
59 |
60 |
61 | def nessus_scan_file_root_element(file):
62 | """
63 | Function returns the root element for tree of given nessus file with scan results.
64 | :param file: given nessus file
65 | :return: root element for this tree.
66 | """
67 |
68 | nessus_scan_file_parsed = parse(file)
69 | root = nessus_scan_file_parsed.getroot()
70 | return root
71 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | This document records all notable changes to [nessus file reader (NFR) by LimberDuck][1].
4 |
5 | Visit [LimberDuck.org][2] to find out more!
6 |
7 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9 |
10 | ## [0.7.1] - 2025-09-01
11 |
12 | ### Added
13 |
14 | - Requirements update
15 | - new:
16 | - packaging>=25.0
17 |
18 | ## [0.7.0] - 2025-09-01
19 |
20 | ### Added
21 |
22 | #### CLI
23 |
24 | - New option:
25 | - `nfr --update-check` / `nfr -u` - will return confirmation if you are using the latest version of NFR.
26 |
27 | - Requirements update
28 | - new:
29 | - requests>=2.32.5
30 |
31 | ## [0.6.0] - 2025-06-28
32 |
33 | ### Added
34 |
35 | #### CLI
36 |
37 | New options for `nfr scan` command:
38 |
39 | - `--plugin-severity` - to list for every detected plugin: Severity, Risk Factor, CVSSv2, CVSSv3, CVSSv4, VPR, EPSS.
40 | - `--plugin-severity-legend` - description for all columns returned by `--plugin-severity`.
41 | - `--filter` `-f` - possibility to filter data returned by `--plugin-severity` to specific values. Read about [JMESPath](https://jmespath.org).
42 |
43 | #### Module
44 |
45 | New functions for plugins:
46 | - `severity_number_to_label(severity_number)` - Convert a numeric severity level to its corresponding string label.
47 | - `cvssv2_score_to_severity(cvss_score)` - Convert a CVSS v2 base score to its corresponding severity label.
48 | - `cvssv3_score_to_severity(cvss_score)` - Convert a CVSS v3 base score to its corresponding severity label.
49 | - `cvssv4_score_to_severity(cvss_score)` - Convert a CVSS v4 base score to its corresponding severity label.
50 | - `vpr_score_to_severity(vpr_score)` - Convert a VPR (Vulnerability Priority Rating) score to its corresponding severity label.
51 | - `epss_score_decimal_to_percent(epss_score)` - Convert an EPSS (Exploit Prediction Scoring System) score from decimal format to a percentage string.
52 |
53 | ### Changed
54 |
55 | - requirements update
56 | - from:
57 | - click>=8.1.8
58 | - to:
59 | - click>=8.2.1
60 | - jmespath>=1.0.1
61 |
62 | - tests for python
63 | - removed: 3.8, 3.9 due to [click 8.2.0 requirements](https://click.palletsprojects.com/en/stable/changes/#version-8-2-0).
64 |
65 | ## [0.5.0] - 2025-05-03
66 |
67 | ### Added
68 |
69 | - Splitting the file with Nessus scan results into smaller files.
70 |
71 | ## [0.4.3] - 2025-02-19
72 |
73 | ### Changed
74 |
75 | - code formatted with [black](https://black.readthedocs.io)
76 | - requirements update
77 | - from:
78 | - click>=8.1.3
79 | - tabulate>=0.8.9
80 | - to:
81 | - click>=8.1.8
82 | - tabulate>=0.9.0
83 |
84 | - tests for python
85 | - added: 3.10, 3.11, 3.12, 3.13
86 | - removed: 3.7
87 |
88 | ## [0.4.2] - 2023-03-04
89 |
90 | ### Changed
91 |
92 | - [README.md](README.md) updated with example `nfr` commadline usage.
93 | - `nfr scan --scan-summary` has simplified column names, to save space on the screen:
94 | - `nessus_scan_file` -> `File name`
95 | - `report_name` -> `Report name`
96 | - `number_of_target_hosts` -> `TH`
97 | - `number_of_scanned_hosts` -> `SH`
98 | - `number_of_scanned_hosts_with_credentialed_checks_yes` -> `CC`
99 | - `nfr scan --scan-summary` has 5 new columns
100 | - `C`, `H`, `M`, `L`, `N`, accordingly number of plugins with Critical, High, Medium, Low and None risk factor for whole scan
101 | - `nfr scan --scan-summary-legend` command to see columns description
102 | - `nfr scan --policy-name` option changed to `--policy-summary`
103 | - `nfr scan --policy-summary` informs about Policy name and settings like Max hosts, Max checks, Check timeout,
104 | Plugins number used during the scan.
105 | - `nfr scan --source-of-file` option changed to `--scan-file-source`
106 |
107 | ### Fixed
108 |
109 | - `detected_os()` function in `host.py` handles situation if there is no Operating System detected
110 | (reported by [ricardosupo](https://github.com/ricardosupo) in issue
111 | [#8](https://github.com/LimberDuck/nessus-file-reader/issues/8#issue-1236020632)).
112 | - `nfr` CLI handles `FileNotFoundError` when you give nessus files or directory which doesn't exist.
113 |
114 | ## [0.4.1] - 2022-05-13
115 |
116 | ### Fixed
117 |
118 | - requirements installation fixed
119 |
120 | ## [0.4.0] - 2022-05-13
121 |
122 | ### Added
123 |
124 | - **commandline interface** - from now on this package will provide you possibility to run `nfr` in commandline. After installation type `nfr` or `nfr --help` to find out more.
125 | - **Tenable.io files support** - initial support to pars nessus files coming from Tenable.io
126 |
127 |
128 | ## [0.3.0] - 2020-07-25
129 |
130 | ### Added
131 |
132 | - new function host.netbios_network_name - to get NetBIOS Computer Name, Workgroup / Domain name for given target.
133 |
134 | ### Changed
135 |
136 | - possibility to pars network address with mask in target
137 |
138 | ## [0.2.0] - 2019-09-09
139 |
140 | ### Added
141 |
142 | - new function plugin.report_item_values - to get list of values for all items with given name e.g. 'cve'
143 |
144 |
145 | ## [0.1.0] - 2019-06-23
146 |
147 | - Initial release
148 |
149 | [0.7.1]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.7.0...v0.7.1
150 | [0.7.0]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.6.0...v0.7.0
151 | [0.6.0]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.5.0...v0.6.0
152 | [0.5.0]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.4.3...v0.5.0
153 | [0.4.3]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.4.2...v0.4.3
154 | [0.4.2]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.4.1...v0.4.2
155 | [0.4.1]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.4.0...v0.4.1
156 | [0.4.0]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.3.0...v0.4.0
157 | [0.3.0]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.2.0...v0.3.0
158 | [0.2.0]: https://github.com/LimberDuck/nessus-file-reader/compare/v0.1.0...v0.2.0
159 | [0.1.0]: https://github.com/LimberDuck/nessus-file-reader/releases/tag/v0.1.0
160 |
161 | [1]: https://github.com/LimberDuck/nessus-file-reader
162 | [2]: https://limberduck.org
--------------------------------------------------------------------------------
/examples/nfr_example_script.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | nessus file reader (NFR) by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a python module
4 | created to quickly parse nessus files containing the results of scans
5 | performed by using Nessus by (C) Tenable, Inc.
6 | Copyright (C) 2019 Damian Krawczyk
7 |
8 | This program is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation, either version 3 of the License, or
11 | (at your option) any later version.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 | """
21 |
22 | import nessus_file_reader as nfr
23 | import os
24 | import glob
25 | import traceback
26 | import time
27 |
28 |
29 | def nfr_example_simple():
30 |
31 | # Provide directory path where nessus files are placed or exact path to one nessus scan file
32 | # default path is current directory
33 | nessus_scan_files = "."
34 |
35 | if os.path.isdir(nessus_scan_files):
36 | os_separator = os.path.sep
37 | extension = "*.nessus"
38 | list_of_source_files = glob.glob(
39 | nessus_scan_files + os_separator + "**" + os_separator + extension,
40 | recursive=True,
41 | )
42 | print(f"Source file path:\n{nessus_scan_files}\n")
43 | print("\nList of source files:")
44 | for source_file in list_of_source_files:
45 | print(f" {source_file}")
46 | else:
47 | list_of_source_files = [nessus_scan_files]
48 | print(f"Source file path:\n{os.path.dirname(nessus_scan_files)}")
49 | print(f"\nList of source files:\n{nessus_scan_files}\n")
50 |
51 | start_time = time.time()
52 | for row_index, nessus_scan_file in enumerate(list_of_source_files):
53 | if os.path.isfile(nessus_scan_file):
54 |
55 | print(
56 | f"\n@-[{str(row_index+1)}/{str(len(list_of_source_files))}]----------------------"
57 | f"-------------------------------------------------------------------------------"
58 | )
59 | try:
60 |
61 | # Use *file* functions to get details about provided file e.g. root, file name, file size.
62 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
63 | file_name = nfr.file.nessus_scan_file_name_with_path(nessus_scan_file)
64 | file_size = nfr.file.nessus_scan_file_size_human(nessus_scan_file)
65 | print(f"File name: {file_name}")
66 | print(f"File size: {file_size}")
67 | print("")
68 |
69 | # Use *scan* functions to get details about provided scan e.g. report name,
70 | # number of target/scanned/credentialed hosts, scan time start/end/elapsed and more.
71 | scan_file_source = nfr.scan.scan_file_source(root)
72 | print(f" Source of file: {scan_file_source}")
73 | report_hosts = nfr.scan.report_hosts(root)
74 | print(f" Report hosts: {report_hosts}")
75 | report_name = nfr.scan.report_name(root)
76 | policy_name = nfr.scan.policy_name(root)
77 | print(f" Report name: {report_name}")
78 | print(f" Policy name: {policy_name}")
79 | number_of_target_hosts = nfr.scan.number_of_target_hosts(root)
80 | print(f" Number of target: {number_of_target_hosts}")
81 | number_of_scanned_hosts = nfr.scan.number_of_scanned_hosts(root)
82 | print(f" Number of scanned: {number_of_scanned_hosts}")
83 | number_of_scanned_hosts_with_credentialed_checks_yes = (
84 | nfr.scan.number_of_scanned_hosts_with_credentialed_checks_yes(root)
85 | )
86 | print(
87 | f" Number of credentialed hosts: {number_of_scanned_hosts_with_credentialed_checks_yes}"
88 | )
89 |
90 | scan_time_start = nfr.scan.scan_time_start(root)
91 | scan_time_end = nfr.scan.scan_time_end(root)
92 | scan_time_elapsed = nfr.scan.scan_time_elapsed(root)
93 | print(
94 | f" Scan time START - END (ELAPSED): {scan_time_start} - {scan_time_end} ({scan_time_elapsed})"
95 | )
96 | print("")
97 |
98 | # Use *host* functions to get details about hosts from provided scan e.g. report hosts names,
99 | # operating system, hosts scan time start/end/elapsed, number of Critical/High/Medium/Low/None findings
100 | # and more.
101 | for report_host in nfr.scan.report_hosts(root):
102 | report_host_name = nfr.host.report_host_name(report_host)
103 | report_host_os = nfr.host.detected_os(report_host)
104 | report_host_scan_time_start = nfr.host.host_time_start(report_host)
105 | report_host_scan_time_end = nfr.host.host_time_end(report_host)
106 | report_host_scan_time_elapsed = nfr.host.host_time_elapsed(
107 | report_host
108 | )
109 | report_host_critical = nfr.host.number_of_plugins_per_risk_factor(
110 | report_host, "Critical"
111 | )
112 | report_host_high = nfr.host.number_of_plugins_per_risk_factor(
113 | report_host, "High"
114 | )
115 | report_host_medium = nfr.host.number_of_plugins_per_risk_factor(
116 | report_host, "Medium"
117 | )
118 | report_host_low = nfr.host.number_of_plugins_per_risk_factor(
119 | report_host, "Low"
120 | )
121 | report_host_none = nfr.host.number_of_plugins_per_risk_factor(
122 | report_host, "None"
123 | )
124 | print(f" Report host name: {report_host_name}")
125 | print(f" Report host OS: {report_host_os}")
126 | print(
127 | f" Host scan time START - END (ELAPSED): "
128 | f"{report_host_scan_time_start} - {report_host_scan_time_end} "
129 | f"({report_host_scan_time_elapsed})"
130 | )
131 | print(
132 | f" Critical/High/Medium/Low/None findings: {report_host_critical}/{report_host_high}/"
133 | f"{report_host_medium}/{report_host_low}/{report_host_none}"
134 | )
135 | print("")
136 |
137 | # Use *plugin* functions to get details about plugins reported in provided scan e.g. plugins ID,
138 | # plugins risk factor, plugins name.
139 | print("\tPlugin ID\t\tRisk Factor\t\t\t\tPlugin Name")
140 | report_items_per_host = nfr.host.report_items(report_host)
141 | for report_item in report_items_per_host:
142 | plugin_id = int(
143 | nfr.plugin.report_item_value(report_item, "pluginID")
144 | )
145 | risk_factor = nfr.plugin.report_item_value(
146 | report_item, "risk_factor"
147 | )
148 | plugin_name = nfr.plugin.report_item_value(
149 | report_item, "pluginName"
150 | )
151 | plugin_cves = nfr.plugin.report_item_values(report_item, "cve")
152 | print(
153 | "\t",
154 | plugin_id,
155 | " \t\t\t",
156 | risk_factor,
157 | " \t\t\t",
158 | plugin_name,
159 | " \t\t\t",
160 | plugin_cves,
161 | )
162 |
163 | print()
164 | # If you want to get output for interesting you plugin
165 | # e.g. "Nessus Scan Information" use below function
166 | pido_19506 = nfr.plugin.plugin_output(root, report_host, "19506")
167 | print(f"Nessus Scan Information Plugin Output:\n{pido_19506}")
168 |
169 | # If you know that interesting you plugin occurs more than ones for particular host
170 | # e.g. "Netstat Portscanner (SSH)" use below function
171 | pidos_14272 = nfr.plugin.plugin_outputs(root, report_host, "14272")
172 | print(
173 | f"All findings for Netstat Portscanner (SSH): \n{pidos_14272}"
174 | )
175 |
176 | netbios_network_name = nfr.host.netbios_network_name(
177 | root, report_host
178 | )
179 | print(f"Netbios network name {netbios_network_name}")
180 |
181 | except Exception as e:
182 | print(f"\nUps... ERROR occurred. \n\n {str(e)}")
183 | traceback.print_exc()
184 | print(
185 | f"ERROR Parsing [{str(row_index+1)}/{str(len(list_of_source_files))}] nessus files"
186 | )
187 |
188 | else:
189 | print(
190 | f"Ups.. {nessus_scan_file} does not exist in current directory: {os.getcwd()}"
191 | )
192 |
193 | end_time = time.time()
194 | elapsed_time = end_time - start_time
195 | elapsed_time_parsed = time.strftime("%H:%M:%S", time.gmtime(elapsed_time))
196 | print(
197 | "\n/===================================================="
198 | "======================================================="
199 | )
200 | print(f'[x] Parsing ended on {time.strftime("%c", time.localtime(end_time))}\n')
201 | print(f"Elapsed time: {elapsed_time_parsed}")
202 |
203 |
204 | def main():
205 |
206 | app_name = nfr.__about__.__title__
207 | app_version = nfr.__about__.__version__
208 | app_version_release_date = nfr.__about__.__release_date__
209 |
210 | print(
211 | f"This is example script for {app_name} {app_version} {app_version_release_date}\n"
212 | )
213 |
214 | nfr_example_simple()
215 |
216 |
217 | main()
218 |
--------------------------------------------------------------------------------
/nessus_file_reader/host/host.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | nessus file reader (NFR) by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a python module
4 | created to quickly parse nessus files containing the results of scans
5 | performed by using Nessus by (C) Tenable, Inc.
6 | Copyright (C) 2019 Damian Krawczyk
7 |
8 | This program is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation, either version 3 of the License, or
11 | (at your option) any later version.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 | """
21 |
22 | import re
23 | import datetime
24 | from nessus_file_reader.plugin import plugin
25 |
26 |
27 | def report_host_name(report_host):
28 | """
29 | Function returns name of given report host.
30 | :param report_host: report host element
31 | :return: name of given report host
32 | """
33 | name = report_host.get("name")
34 | return name
35 |
36 |
37 | def host_property_value(report_host, property_name):
38 | """
39 | Function returns value of given property for given target, e.g. hostname.
40 | :param report_host: report host element
41 | :param property_name: exact property name
42 | :return: property value
43 | """
44 | property_exist = report_host[0].find("tag/[@name='" + property_name + "']")
45 |
46 | if property_exist is not None:
47 | property_value = property_exist.text
48 | else:
49 | property_value = None
50 | return property_value
51 |
52 |
53 | def resolved_hostname(report_host):
54 | """
55 | Function returns hostname for given target. If hostname contains FQDN only hostname will be returned.
56 | :param report_host: report host element
57 | :return: hostname for given target
58 | """
59 | hostname = host_property_value(report_host, "hostname")
60 | if hostname is not None:
61 | hostname = hostname.lower()
62 | else:
63 | hostname = ""
64 | return hostname.split(".")[0]
65 |
66 |
67 | def resolved_ip(report_host):
68 | """
69 | Function returns ip for given target.
70 | :param report_host: report host element
71 | :return: ip for given target
72 | """
73 | host_ip = host_property_value(report_host, "host-ip")
74 | return host_ip
75 |
76 |
77 | def resolved_fqdn(report_host):
78 | """
79 | Function returns fqdn for given target.
80 | :param report_host: report host element
81 | :return: fqdn for given target
82 | """
83 | host_fqdn = host_property_value(report_host, "host-fqdn")
84 | if host_fqdn is not None:
85 | host_fqdn = host_fqdn.lower()
86 | return host_fqdn
87 |
88 |
89 | def netbios_network_name(root, report_host):
90 | """
91 | Function returns information about NetBIOS Computer Name, Workgroup / Domain name for given target.
92 | :param root: root element of scan file tree
93 | :param report_host: report host element
94 | :return: os for given target
95 | """
96 | pido_10150 = plugin.plugin_output(root, report_host, "10150")
97 | pido_10150_split = pido_10150.split("\n")
98 |
99 | netbios_computer_name = ""
100 | netbios_domain_name = ""
101 | for netbios_data_split_entry in pido_10150_split:
102 | if "Computer name" in netbios_data_split_entry:
103 | netbios_computer_name = (
104 | netbios_data_split_entry.split("=")[0].strip().lower()
105 | )
106 |
107 | if "Workgroup / Domain name" in netbios_data_split_entry:
108 | netbios_domain_name = netbios_data_split_entry.split("=")[0].strip().lower()
109 |
110 | return {
111 | "netbios_computer_name": netbios_computer_name,
112 | "netbios_domain_name": netbios_domain_name,
113 | }
114 |
115 |
116 | def detected_os(report_host):
117 | """
118 | Function returns information about Operating System for given target.
119 | :param report_host: report host element
120 | :return: os for given target
121 | """
122 | operating_system = host_property_value(report_host, "operating-system")
123 | if operating_system is not None:
124 | if """ in operating_system:
125 | operating_system = str(operating_system).strip("["").strip(""]")
126 | else:
127 | operating_system = str(operating_system).strip('["').strip('"]')
128 | else:
129 | operating_system = ""
130 | return operating_system
131 |
132 |
133 | def scanner_ip(root, report_host):
134 | """
135 | Function returns scanner ip for given target based on Plugin ID 19506.
136 | :param root: root element of scan file tree
137 | :param report_host: report host element
138 | :return: ip address of scanner
139 | """
140 | ip = None
141 | pido_19506 = plugin.plugin_output(root, report_host, "19506")
142 | for line in pido_19506.split("\n"):
143 | if re.findall("Scanner IP :", line):
144 | ip = re.sub("Scanner IP : ", "", line)
145 | return ip
146 |
147 |
148 | def login_used(report_host):
149 | """
150 | Function returns login name used during scan for given target.
151 | :param report_host: report host element
152 | :return: login name
153 | """
154 | login = None
155 |
156 | for tag in report_host[0].findall("tag"):
157 | tag_name = tag.get("name")
158 | if re.findall("login-used", tag_name):
159 | if tag_name is not None:
160 | login = tag.text
161 | return login
162 |
163 |
164 | def credentialed_checks(root, report_host):
165 | """
166 | Function returns confirmation if credentialed checks have been enabled during scan for given target based on
167 | Plugin ID 19506.
168 | :param root: root element of scan file tree
169 | :param report_host: report host element
170 | :return:
171 | 'yes' + login used - if credentialed checks have been enabled
172 | 'no' - if credentialed checks have not been enabled
173 | """
174 | credentialed = None
175 | pido_19506 = plugin.plugin_output(root, report_host, "19506")
176 | if (
177 | "No output recorded." in pido_19506
178 | or "Check Audit Trail" in pido_19506
179 | or "19506 not enabled." in pido_19506
180 | ):
181 | credentialed = "no"
182 | else:
183 | for line in pido_19506.split("\n"):
184 | if re.findall("Credentialed checks :", line):
185 | credentialed = re.sub("Credentialed checks : ", "", line)
186 | credentialed = re.sub("'", "", credentialed)
187 |
188 | return credentialed
189 |
190 |
191 | def credentialed_checks_db(root, report_host):
192 | """
193 | Function returns confirmation if credentialed checks have been enabled during scan for given target based on
194 | Plugin IDs 91825 and 91827.
195 | :param root: root element of scan file tree
196 | :param report_host: report host element
197 | :return:
198 | 'yes' + info about source - if credentialed checks have been enabled
199 | 'no' - if credentialed checks have not been enabled
200 | """
201 | credentialed = None
202 | # "91825: Oracle DB Login Possible"
203 | pido_91825 = plugin.plugin_output(root, report_host, "91825")
204 | if (
205 | "No output recorded." in pido_91825
206 | or "Check Audit Trail" in pido_91825
207 | or "91825 not enabled." in pido_91825
208 | ):
209 | credentialed = "no"
210 | elif re.findall(
211 | "Credentialed checks have been enabled for Oracle RDBMS server", pido_91825
212 | ):
213 | credentialed = "yes, based on plugin id 91825"
214 |
215 | # "91827: Microsoft SQL Server Login Possible"
216 | pido_91827 = plugin.plugin_output(root, report_host, "91827")
217 | if (
218 | "No output recorded." in pido_91827
219 | or "Check Audit Trail" in pido_91827
220 | or "91827 not enabled." in pido_91827
221 | ):
222 | credentialed = "no"
223 | elif re.findall(
224 | "Credentialed checks have been enabled for MSSQL server", pido_91827
225 | ):
226 | credentialed = "yes, based on plugin id 91827"
227 |
228 | return credentialed
229 |
230 |
231 | def number_of_plugins(report_host):
232 | """
233 | Function returns number of reported plugins for given target.
234 | :param report_host: report host element
235 | :return: number of reported plugins
236 | """
237 | number_of_plugins_counter = len(report_host.findall("ReportItem"))
238 | return number_of_plugins_counter
239 |
240 |
241 | def number_of_plugins_per_risk_factor(report_host, risk_factor_level):
242 | """
243 | Function returns number of all plugins reported during scan for given risk factor for given target.
244 | :param report_host: report host element
245 | :param risk_factor_level:
246 | 'Critical'
247 | 'High'
248 | 'Medium'
249 | 'Low'
250 | 'None'
251 | :return: number of plugins for given risk factor
252 | """
253 | risk_factor_counter = 0
254 | for report_item in report_host.findall("ReportItem"):
255 | risk_factor = report_item.find("risk_factor")
256 | if risk_factor is not None:
257 | if risk_factor.text == risk_factor_level:
258 | risk_factor_counter += 1
259 | return risk_factor_counter
260 |
261 |
262 | def number_of_compliance_plugins(report_host):
263 | """
264 | Function returns number of reported compliance plugins for given target.
265 | :param report_host: report host element
266 | :return: number of reported compliance plugins
267 | """
268 | compliance_plugin_counter = 0
269 | for report_item in report_host.findall("ReportItem"):
270 | compliance = report_item.find("compliance")
271 | if compliance is not None:
272 | if compliance.text == "true":
273 | compliance_plugin_counter += 1
274 | return compliance_plugin_counter
275 |
276 |
277 | def number_of_compliance_plugins_per_result(report_host, compliance_result):
278 | """
279 | Function returns number of all compliance plugins reported during scan for given compliance result for given target.
280 | :param report_host: report host element
281 | :param compliance_result:
282 | 'PASSED'
283 | 'FAILED'
284 | 'WARNING'
285 | :return: number of compliance plugins for given compliance result
286 | """
287 | compliance_counter = 0
288 | for report_item in report_host.findall("ReportItem"):
289 | compliance = report_item.find(
290 | "cm:compliance-result", namespaces={"cm": "http://www.nessus.org/cm"}
291 | )
292 | if compliance is not None:
293 | if compliance.text == compliance_result:
294 | compliance_counter += 1
295 | return compliance_counter
296 |
297 |
298 | def report_items(report_host):
299 | """
300 | Function returns all items for given target.
301 | :param report_host: report host element
302 | :return: list of report items
303 | """
304 | items = report_host.findall("ReportItem")
305 | return items
306 |
307 |
308 | def host_time_start(report_host):
309 | """
310 | Function returns scan start time for given target.
311 | :param report_host: report host element
312 | :return: formatted date and time when scan has been started
313 | """
314 | host_start_time = host_property_value(report_host, "HOST_START")
315 | if host_start_time is not None:
316 | host_start_time_formatted = datetime.datetime.strptime(
317 | host_start_time, "%a %b %d %H:%M:%S %Y"
318 | )
319 | else:
320 | host_start_time_formatted = None
321 | return host_start_time_formatted
322 |
323 |
324 | def host_time_end(report_host):
325 | """
326 | Function returns scan end time for given target.
327 | :param report_host: report host element
328 | :return: formatted date and time when scan has been ended
329 | """
330 | host_end_time = host_property_value(report_host, "HOST_END")
331 | if host_end_time is not None:
332 | host_end_time_formatted = datetime.datetime.strptime(
333 | host_end_time, "%a %b %d %H:%M:%S %Y"
334 | )
335 | else:
336 | host_end_time_formatted = None
337 |
338 | return host_end_time_formatted
339 |
340 |
341 | def host_time_elapsed(report_host):
342 | """
343 | Function returns scan time elapsed in format HH:MM:SS for given target.
344 | :param report_host: report host element
345 | :return: scan time elapsed in format HH:MM:SS.
346 | """
347 | host_time_start_value = host_time_start(report_host)
348 | host_time_end_value = host_time_end(report_host)
349 | if host_time_end_value is not None:
350 | elapsed_time = host_time_end_value - host_time_start_value
351 | elapsed_time = str(elapsed_time)
352 | else:
353 | elapsed_time = None
354 |
355 | return elapsed_time
356 |
--------------------------------------------------------------------------------
/nessus_file_reader/plugin/plugin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | nessus file reader (NFR) by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a python module
4 | created to quickly parse nessus files containing the results of scans
5 | performed by using Nessus by (C) Tenable, Inc.
6 | Copyright (C) 2019 Damian Krawczyk
7 |
8 | This program is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation, either version 3 of the License, or
11 | (at your option) any later version.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 | """
21 |
22 | import re
23 | import datetime
24 | from nessus_file_reader.scan import scan
25 |
26 |
27 | def plugin_output(root, report_host, plugin_id):
28 | """
29 | Function returns plugin output for given plugin id. If particular plugin occurs in report more than once, plugin
30 | output of last occurrence is returned.
31 | :param root: root element of scan file tree
32 | :param report_host: scanned host
33 | :param plugin_id: plugin id
34 | :return:
35 | plugin_output - content of plugin output is returned if plugin occurs in report and has an output.
36 | '{plugin_id} - no output recorded' - information if plugin occurs in report but does not contain any output.
37 | '{plugin_id} - check Audit Trail' - information if plugin has been used during scan but does not appear in report at all.
38 | '{plugin_id} - not enabled' - information if plugin has not been enabled in policy for scan.
39 | '{plugin_id} - info about used plugins not available' - information if plugin_set from policy settings not available.
40 |
41 | """
42 | plugin_id = str(plugin_id)
43 | plugin_output_content = list()
44 | plugin_set = scan.plugin_set(root)
45 | status = 0
46 |
47 | for report_item in report_host.findall("ReportItem"):
48 | plugin_id_from_report = report_item.get("pluginID")
49 | if plugin_id_from_report == plugin_id:
50 | plugin_output_item = report_item.find("plugin_output")
51 | if plugin_output_item is None:
52 | plugin_output_content = f"{plugin_id} - no output recorded"
53 | else:
54 | plugin_output_content = plugin_output_item.text
55 | status = 1
56 | if status == 0:
57 | plugin_output_content = f"{plugin_id} - check Audit Trail"
58 |
59 | if "check Audit Trail" in plugin_output_content:
60 |
61 | if plugin_set is not None:
62 | if plugin_id not in scan.plugin_set(root):
63 | plugin_output_content = f"{plugin_id} - not enabled"
64 | else:
65 | plugin_output_content = (
66 | f"{plugin_id} - info about used plugins not available"
67 | )
68 | return plugin_output_content
69 |
70 |
71 | def plugin_outputs(root, report_host, plugin_id):
72 | """
73 | Function returns plugin output for given plugin id. If particular plugin occurs in report more than once, plugin
74 | outputs are concatenated and return as one.
75 | :param root: root element of scan file tree
76 | :param report_host: scanned host
77 | :param plugin_id: plugin id
78 | :return:
79 | plugin_output - content of plugin output is returned if plugin occurs in report and has an output.
80 | '{plugin_id} - no output recorded' - information if plugin occurs in report but does not contain any output.
81 | '{plugin_id} - check Audit Trail' - information if plugin has been used during scan but does not appear in report at all.
82 | '{plugin_id} - not enabled' - information if plugin has not been enabled in policy for scan.
83 | '{plugin_id} - info about used plugins not available' - information if plugin_set from policy settings not available.
84 |
85 | """
86 | plugin_id = str(plugin_id)
87 | plugin_output_content = list()
88 | plugin_set = scan.plugin_set(root)
89 | status = 0
90 |
91 | for report_item in report_host.findall("ReportItem"):
92 | plugin_id_from_report = report_item.get("pluginID")
93 | if plugin_id_from_report == plugin_id:
94 | plugin_output_item = report_item.find("plugin_output")
95 | if plugin_output_item is None:
96 | plugin_output_content.append(f"{plugin_id} - no output recorded")
97 | else:
98 | plugin_output_content.append(plugin_output_item.text)
99 | status = 1
100 | if status == 0:
101 | plugin_output_content.append(f"{plugin_id} - check Audit Trail")
102 |
103 | if f"{plugin_id} - check Audit Trail" in plugin_output_content:
104 |
105 | if plugin_set is not None:
106 | if plugin_id not in scan.plugin_set(root):
107 | plugin_output_content = [f"{plugin_id} - not enabled"]
108 | else:
109 | plugin_output_content = [
110 | f"{plugin_id} - info about used plugins not available"
111 | ]
112 |
113 | if len(plugin_output_content) == 1:
114 | plugin_output_content = plugin_output_content[0]
115 | else:
116 | plugin_output_content = "\n".join(plugin_output_content)
117 |
118 | return plugin_output_content
119 |
120 |
121 | def compliance_plugin(report_item):
122 | """
123 | Function checks if given report item is compliance plugin.
124 | :param report_item: particular report item for scanned host
125 | :return:
126 | True if report item is compliance
127 | False if report item is not compliance
128 | """
129 | compliance = report_item_value(report_item, "compliance")
130 | plugin_type_compliance = False
131 | if compliance is not None:
132 | if compliance == "true":
133 | plugin_type_compliance = True
134 | else:
135 | plugin_type_compliance = False
136 |
137 | return plugin_type_compliance
138 |
139 |
140 | def report_item_value(report_item, report_item_name):
141 | """
142 | Function returns value of given report item e.g. pluginName
143 | :param report_item: particular report item for scanned host
144 | :param report_item_name: exact report item name for scanned host
145 | :return: value of given report item
146 | """
147 | report_item_content_value = report_item.get(report_item_name)
148 |
149 | if report_item_content_value is None:
150 | report_item_content = report_item.find(report_item_name)
151 | if report_item_content is not None:
152 | report_item_content_value = report_item_content.text
153 | return report_item_content_value
154 |
155 |
156 | def report_item_values(report_item, report_item_name):
157 | """
158 | Function returns list of all values of given report item e.g. list of CVE numbers
159 | :param report_item: particular report item for scanned host
160 | :param report_item_name: exact report item name for scanned host
161 | :return: value of given report item
162 | """
163 | report_item_values_list = []
164 | report_item_content_values = report_item.findall(report_item_name)
165 | for report_item_content_value in report_item_content_values:
166 | report_item_values_list.append(report_item_content_value.text)
167 | return report_item_values_list
168 |
169 |
170 | def compliance_check_item_value(report_item, compliance_check_item_name):
171 | """
172 | Function returns value of given compliance check item e.g. cm:compliance-check-name
173 | :param report_item: particular report item for scanned host
174 | :param compliance_check_item_name: exact compliance check item name for scanned host
175 | :return: value of given compliance check item name
176 | """
177 | compliance_check_item_content_value = None
178 | compliance = report_item.find("compliance")
179 | if compliance is not None:
180 | if compliance.text == "true":
181 | compliance_check_item_content = report_item.find(
182 | compliance_check_item_name,
183 | namespaces={"cm": "http://www.nessus.org/cm"},
184 | )
185 | if compliance_check_item_content is not None:
186 | compliance_check_item_content_value = compliance_check_item_content.text
187 | return compliance_check_item_content_value
188 |
189 |
190 | def plugin_date(date):
191 | """
192 | Function convert given plugin date e.g. plugin_publication_date
193 | :param date: date from plugin
194 | :return: formatted date
195 | """
196 | date_dash = re.search("\d{4}-\d{2}-\d{2}", date)
197 | date_slash = re.search("\d{4}/\d{2}/\d{2}", date)
198 |
199 | if date_dash:
200 | date_formatted = datetime.datetime.strptime(date, "%Y-%m-%d").date()
201 | elif date_slash:
202 | date_formatted = datetime.datetime.strptime(date, "%Y/%m/%d").date()
203 | else:
204 | date_formatted = None
205 | return date_formatted
206 |
207 |
208 | def severity_number_to_label(severity_number):
209 | """
210 | Convert a numeric severity level to its corresponding string label.
211 |
212 | Parameters:
213 | severity_number: An integer representing the severity level as
214 | defined by Nessus in scan results. Expected values are:
215 | 0 - Informational
216 | 1 - Low
217 | 2 - Medium
218 | 3 - High
219 | 4 - Critical
220 |
221 | Returns:
222 | A string representing the severity level. If the input is not recognized,
223 | returns "Unknown".
224 |
225 | Reference:
226 | https://docs.tenable.com/quick-reference/nessus-file-format/Nessus-File-Format.pdf
227 | """
228 | severity_map = {0: "Info", 1: "Low", 2: "Medium", 3: "High", 4: "Critical"}
229 | return severity_map.get(int(severity_number), "Unknown")
230 |
231 |
232 | def cvssv2_score_to_severity(cvss_score):
233 | """
234 | Convert a CVSS v2 base score to its corresponding severity label.
235 |
236 | Parameters:
237 | cvss_score: A numeric value representing the CVSS v2 base score.
238 | Expected range is 0.0 to 10.0.
239 |
240 | Returns:
241 | A string representing the severity level:
242 | - 0.0 -> "None"
243 | - 0.1-3.9 -> "Low"
244 | - 4.0-6.9 -> "Medium"
245 | - 7.0-9.9 -> "High"
246 | - 10.0 -> "Critical"
247 | If the input is None returns "", if out of range returns "Unknown".
248 |
249 | References:
250 | - https://docs.tenable.com/nessus/10_8/Content/RiskMetrics.htm
251 | - https://docs.tenable.com/security-center/Content/RiskMetrics.htm
252 | """
253 | try:
254 | score = float(cvss_score)
255 | except (ValueError, TypeError):
256 | return ""
257 |
258 | if score == 0.0:
259 | return "None"
260 | elif 0.1 <= score <= 3.9:
261 | return "Low"
262 | elif 4.0 <= score <= 6.9:
263 | return "Medium"
264 | elif 7.0 <= score <= 9.9:
265 | return "High"
266 | elif score == 10.0:
267 | return "Critical"
268 | else:
269 | return "Unknown"
270 |
271 |
272 | def cvssv3_score_to_severity(cvss_score):
273 | """
274 | Convert a CVSS v3 base score to its corresponding severity label.
275 |
276 | Parameters:
277 | cvss_score: A numeric value representing the CVSS v3 base score.
278 | Expected range is 0.0 to 10.0.
279 |
280 | Returns:
281 | A string representing the severity level:
282 | - 0.0 -> "None"
283 | - 0.1-3.9 -> "Low"
284 | - 4.0-6.9 -> "Medium"
285 | - 7.0-8.9 -> "High"
286 | - 9.0-10.0 -> "Critical"
287 | If the input is None returns "", if out of range returns "Unknown".
288 |
289 | References:
290 | - https://docs.tenable.com/nessus/10_8/Content/RiskMetrics.htm
291 | - https://docs.tenable.com/security-center/Content/RiskMetrics.htm
292 | """
293 | try:
294 | score = float(cvss_score)
295 | except (ValueError, TypeError):
296 | return ""
297 |
298 | if score == 0.0:
299 | return "None"
300 | elif 0.1 <= score <= 3.9:
301 | return "Low"
302 | elif 4.0 <= score <= 6.9:
303 | return "Medium"
304 | elif 7.0 <= score <= 8.9:
305 | return "High"
306 | elif 9.0 <= score <= 10.0:
307 | return "Critical"
308 | else:
309 | return "Unknown"
310 |
311 |
312 | def cvssv4_score_to_severity(cvss_score):
313 | """
314 | Convert a CVSS v4 base score to its corresponding severity label.
315 |
316 | Parameters:
317 | cvss_score: A numeric value representing the CVSS v4 base score.
318 | Expected range is 0.0 to 10.0.
319 |
320 | Returns:
321 | A string representing the severity level:
322 | - 0.0 -> "None"
323 | - 0.1-3.9 -> "Low"
324 | - 4.0-6.9 -> "Medium"
325 | - 7.0-8.9 -> "High"
326 | - 9.0-10.0 -> "Critical"
327 | If the input is None returns "", if out of range returns "Unknown".
328 |
329 | Reference:
330 | https://docs.tenable.com/nessus/10_8/Content/RiskMetrics.htm
331 | """
332 |
333 | try:
334 | score = float(cvss_score)
335 | except (ValueError, TypeError):
336 | return ""
337 |
338 | if score == 0.0:
339 | return "None"
340 | elif 0.1 <= score <= 3.9:
341 | return "Low"
342 | elif 4.0 <= score <= 6.9:
343 | return "Medium"
344 | elif 7.0 <= score <= 8.9:
345 | return "High"
346 | elif 9.0 <= score <= 10.0:
347 | return "Critical"
348 | else:
349 | return "Unknown"
350 |
351 |
352 | def vpr_score_to_severity(vpr_score):
353 | """
354 | Convert a VPR (Vulnerability Priority Rating) score to its corresponding severity label.
355 |
356 | Parameters:
357 | cvss_score: A numeric value representing the VPR score,
358 | typically in the range of 0.0 to 10.0.
359 |
360 | Returns:
361 | A string representing the severity level:
362 | - 0.0 -> "None"
363 | - 0.1-3.9 -> "Low"
364 | - 4.0-6.9 -> "Medium"
365 | - 7.0-8.9 -> "High"
366 | - 9.0-10.0 -> "Critical"
367 | If the input is None returns "", if out of range returns "Unknown".
368 |
369 | References:
370 | - https://docs.tenable.com/nessus/10_8/Content/RiskMetrics.htm
371 | - https://docs.tenable.com/security-center/Content/RiskMetrics.htm
372 | """
373 | try:
374 | score = float(vpr_score)
375 | except (ValueError, TypeError):
376 | return ""
377 |
378 | if score == 0.0:
379 | return "None"
380 | elif 0.1 <= score <= 3.9:
381 | return "Low"
382 | elif 4.0 <= score <= 6.9:
383 | return "Medium"
384 | elif 7.0 <= score <= 8.9:
385 | return "High"
386 | elif 9.0 <= score <= 10.0:
387 | return "Critical"
388 | else:
389 | return "Unknown"
390 |
391 |
392 | def epss_score_decimal_to_percent(epss_score):
393 | """
394 | Convert an EPSS (Exploit Prediction Scoring System) score from decimal format to a percentage string.
395 |
396 | Parameters:
397 | epss_score: A numeric value representing the EPSS score in decimal format,
398 | typically between 0.0 and 1.0 (e.g., 0.153).
399 |
400 | Returns:
401 | A string representing the EPSS score as a percentage with one decimal place (e.g., "15.3%").
402 | If the input is None returns "".
403 |
404 | References:
405 | - https://docs.tenable.com/nessus/10_8/Content/Severity.htm
406 | - https://www.first.org/epss/articles/prob_percentile_bins
407 | """
408 | try:
409 | score = float(epss_score)
410 | except (ValueError, TypeError):
411 | return ""
412 |
413 | return f"{score * 100:.1f}%"
414 |
--------------------------------------------------------------------------------
/nessus_file_reader/__main__.py:
--------------------------------------------------------------------------------
1 | from nessus_file_reader._version import __version__
2 | import click
3 | import nessus_file_reader as nfr
4 | from nessus_file_reader import utilities, __about__
5 | import os
6 | import glob
7 | import tabulate
8 | import jmespath
9 |
10 |
11 | def print_version(ctx, param, value):
12 | if not value or ctx.resilient_parsing:
13 | return
14 | click.echo("Version {}".format(__version__))
15 | ctx.exit()
16 |
17 |
18 | _file_arguments = [
19 | click.argument(
20 | "files",
21 | nargs=-1,
22 | type=click.Path(),
23 | )
24 | ]
25 |
26 |
27 | def add_options(options):
28 | def _add_options(func):
29 | for option in reversed(options):
30 | func = option(func)
31 | return func
32 |
33 | return _add_options
34 |
35 |
36 | def add_arguments(arguments):
37 | def _add_arguments(func):
38 | for argument in reversed(arguments):
39 | func = argument(func)
40 | return func
41 |
42 | return _add_arguments
43 |
44 |
45 | PACKAGE_NAME = __about__.__package_name__
46 |
47 |
48 | @click.group(
49 | invoke_without_command=True,
50 | help="NFR - CLI tool and python module to pars nessus files",
51 | epilog=f"Additional information:\n\n"
52 | f"https://limberduck.org/en/latest/tools/{PACKAGE_NAME}\n"
53 | f"https://github.com/LimberDuck/{PACKAGE_NAME}\n"
54 | f"https://github.com/LimberDuck/{PACKAGE_NAME}/releases\n",
55 | )
56 | @click.option(
57 | "--version",
58 | "-v",
59 | is_flag=True,
60 | callback=print_version,
61 | expose_value=False,
62 | is_eager=True,
63 | )
64 | @click.option(
65 | "--update-check",
66 | "-u",
67 | is_flag=True,
68 | help="Check if a new version is available and exit.",
69 | )
70 | @click.pass_context
71 | def cli(ctx, update_check):
72 | if ctx.invoked_subcommand is None and not update_check:
73 | click.echo(ctx.get_help())
74 | ctx.exit(0)
75 | if ctx.invoked_subcommand is None and update_check:
76 | utilities.check_for_update()
77 |
78 |
79 | @cli.command()
80 | @add_arguments(_file_arguments)
81 | @click.option("--size", is_flag=True, help="file size")
82 | @click.option("--structure", is_flag=True, help="file structure")
83 | @click.option(
84 | "--split", type=int, help="file split into batches per number of ReportHost"
85 | )
86 | def file(files, size, structure, split):
87 | """Options related to nessus file."""
88 |
89 | for file in files:
90 |
91 | if size:
92 | try:
93 | if os.path.isdir(file):
94 | os_separator = os.path.sep
95 | extension = "*.nessus"
96 | list_of_source_files = glob.glob(
97 | file + os_separator + "**" + os_separator + extension,
98 | recursive=True,
99 | )
100 | else:
101 | list_of_source_files = [file]
102 | # print('')
103 | for row_index, nessus_scan_file in enumerate(list_of_source_files):
104 | file_name_with_path = nfr.file.nessus_scan_file_name_with_path(
105 | nessus_scan_file
106 | )
107 | file_size = nfr.file.nessus_scan_file_size_human(
108 | file_name_with_path
109 | )
110 | print(nessus_scan_file, file_size)
111 | except FileNotFoundError as e:
112 | print(e.strerror)
113 |
114 | elif structure:
115 | try:
116 | if os.path.isdir(file):
117 | os_separator = os.path.sep
118 | extension = "*.nessus"
119 | list_of_source_files = glob.glob(
120 | file + os_separator + "**" + os_separator + extension,
121 | recursive=True,
122 | )
123 | else:
124 | list_of_source_files = [file]
125 | for row_index, nessus_scan_file in enumerate(list_of_source_files):
126 |
127 | print(nessus_scan_file)
128 | utilities.nessus_scan_file_structure(nessus_scan_file)
129 | except FileNotFoundError as e:
130 | print(e.strerror)
131 | elif split:
132 | try:
133 | if os.path.isdir(file):
134 | os_separator = os.path.sep
135 | extension = "*.nessus"
136 | list_of_source_files = glob.glob(
137 | file + os_separator + "**" + os_separator + extension,
138 | recursive=True,
139 | )
140 | else:
141 | list_of_source_files = [file]
142 | for row_index, nessus_scan_file in enumerate(list_of_source_files):
143 |
144 | print(nessus_scan_file)
145 | utilities.nessus_scan_file_split(nessus_scan_file, split)
146 | except FileNotFoundError as e:
147 | print(e.strerror)
148 | else:
149 | print("No parameters specified")
150 |
151 |
152 | @cli.command()
153 | @add_arguments(_file_arguments)
154 | @click.option("--scan-summary", is_flag=True, help="Scan summary")
155 | @click.option("--scan-summary-legend", is_flag=True, help="Show scan summary legend")
156 | @click.option("--plugin-severity", is_flag=True, help="Plugin severity")
157 | @click.option(
158 | "--plugin-severity-legend", is_flag=True, help="Show plugin severity legend"
159 | )
160 | @click.option("--policy-summary", is_flag=True, help="Policy summary")
161 | @click.option(
162 | "--scan-file-source",
163 | is_flag=True,
164 | help="Source of scan file e.g. Nessus, Tenable.sc, Tenable.io",
165 | )
166 | @click.option(
167 | "--filter",
168 | "-f",
169 | help="filter data with JMESPath. See https://jmespath.org/ for more information and examples. "
170 | "Works with --plugin-severity only. ",
171 | )
172 | def scan(
173 | files,
174 | scan_summary,
175 | scan_summary_legend,
176 | plugin_severity,
177 | plugin_severity_legend,
178 | scan_file_source,
179 | policy_summary,
180 | filter,
181 | ):
182 | """Options related to content of nessus file on scan level."""
183 |
184 | if files:
185 | try:
186 | summary_data = []
187 | scan_file_source_data = []
188 | policy_summary_data = []
189 | plugin_severity_data = []
190 | for file in files:
191 | if os.path.isdir(file):
192 | os_separator = os.path.sep
193 | extension = "*.nessus"
194 | list_of_source_files = glob.glob(
195 | file + os_separator + "**" + os_separator + extension,
196 | recursive=True,
197 | )
198 | else:
199 | list_of_source_files = [file]
200 |
201 | for row_index, nessus_scan_file in enumerate(list_of_source_files):
202 |
203 | file_name_with_path = nfr.file.nessus_scan_file_name_with_path(
204 | nessus_scan_file
205 | )
206 | file_size = nfr.file.nessus_scan_file_size_human(nessus_scan_file)
207 | # print(nessus_scan_file, file_size)
208 | root = nfr.file.nessus_scan_file_root_element(file_name_with_path)
209 | if policy_summary:
210 | policy_name = nfr.scan.policy_name(root)
211 | policy_max_hosts = nfr.scan.policy_max_hosts(root)
212 | policy_max_checks = nfr.scan.policy_max_checks(root)
213 | policy_checks_read_timeout = (
214 | nfr.scan.policy_checks_read_timeout(root)
215 | )
216 | plugin_set_number = nfr.scan.plugin_set_number(root)
217 | policy_summary_data.append(
218 | {
219 | "File name": nessus_scan_file,
220 | "Policy name": policy_name,
221 | "Max hosts": policy_max_hosts,
222 | "Max checks": policy_max_checks,
223 | "Checks timeout": policy_checks_read_timeout,
224 | "Plugins number": plugin_set_number,
225 | }
226 | )
227 |
228 | if scan_file_source:
229 | scan_file_source_info = nfr.scan.scan_file_source(root)
230 | scan_file_source_data.append(
231 | {
232 | "File name": nessus_scan_file,
233 | "Source": scan_file_source_info,
234 | }
235 | )
236 |
237 | if scan_summary:
238 |
239 | report_name = nfr.scan.report_name(root)
240 | number_of_target_hosts = nfr.scan.number_of_target_hosts(root)
241 | number_of_scanned_hosts = nfr.scan.number_of_scanned_hosts(root)
242 | number_of_scanned_hosts_with_credentialed_checks_yes = nfr.scan.number_of_scanned_hosts_with_credentialed_checks_yes(
243 | root
244 | )
245 |
246 | report_host_critical = 0
247 | report_host_high = 0
248 | report_host_medium = 0
249 | report_host_low = 0
250 | report_host_none = 0
251 |
252 | for report_host in nfr.scan.report_hosts(root):
253 | report_host_critical += (
254 | nfr.host.number_of_plugins_per_risk_factor(
255 | report_host, "Critical"
256 | )
257 | )
258 | report_host_high += (
259 | nfr.host.number_of_plugins_per_risk_factor(
260 | report_host, "High"
261 | )
262 | )
263 | report_host_medium += (
264 | nfr.host.number_of_plugins_per_risk_factor(
265 | report_host, "Medium"
266 | )
267 | )
268 | report_host_low += (
269 | nfr.host.number_of_plugins_per_risk_factor(
270 | report_host, "Low"
271 | )
272 | )
273 | report_host_none += (
274 | nfr.host.number_of_plugins_per_risk_factor(
275 | report_host, "None"
276 | )
277 | )
278 |
279 | summary_data.append(
280 | {
281 | "File name": nessus_scan_file,
282 | "Report name": report_name,
283 | "TH": number_of_target_hosts,
284 | "SH": number_of_scanned_hosts,
285 | "CC": number_of_scanned_hosts_with_credentialed_checks_yes,
286 | "C": report_host_critical,
287 | "H": report_host_high,
288 | "M": report_host_medium,
289 | "L": report_host_low,
290 | "N": report_host_none,
291 | }
292 | )
293 |
294 | if plugin_severity:
295 |
296 | for report_host in nfr.scan.report_hosts(root):
297 | report_host_name = nfr.host.report_host_name(report_host)
298 | report_items_per_host = nfr.host.report_items(report_host)
299 | for report_item in report_items_per_host:
300 | plugin_id = nfr.plugin.report_item_value(
301 | report_item, "pluginID"
302 | )
303 | severity = nfr.plugin.report_item_value(
304 | report_item, "severity"
305 | )
306 | severity_label = nfr.plugin.severity_number_to_label(
307 | severity
308 | )
309 | risk_factor = nfr.plugin.report_item_value(
310 | report_item, "risk_factor"
311 | )
312 | cvssv2_base_score = nfr.plugin.report_item_value(
313 | report_item, "cvss_base_score"
314 | )
315 | cvssv2_base_score_label = (
316 | nfr.plugin.cvssv2_score_to_severity(
317 | cvssv2_base_score
318 | )
319 | )
320 | cvssv3_base_score = nfr.plugin.report_item_value(
321 | report_item, "cvss3_base_score"
322 | )
323 | cvssv3_base_score_label = (
324 | nfr.plugin.cvssv3_score_to_severity(
325 | cvssv3_base_score
326 | )
327 | )
328 | cvssv4_base_score = nfr.plugin.report_item_value(
329 | report_item, "cvss4_base_score"
330 | )
331 | cvssv4_base_score_label = (
332 | nfr.plugin.cvssv4_score_to_severity(
333 | cvssv4_base_score
334 | )
335 | )
336 | vpr_score = nfr.plugin.report_item_value(
337 | report_item, "vpr_score"
338 | )
339 | vpr_score_label = nfr.plugin.vpr_score_to_severity(
340 | vpr_score
341 | )
342 | epss_score = nfr.plugin.report_item_value(
343 | report_item, "epss_score"
344 | )
345 | epss_score_label = (
346 | nfr.plugin.epss_score_decimal_to_percent(epss_score)
347 | )
348 |
349 | plugin_severity_data.append(
350 | {
351 | "File name": nessus_scan_file,
352 | "Report host name": report_host_name,
353 | "PID": plugin_id,
354 | "S": severity,
355 | "SL": severity_label,
356 | "RF": risk_factor,
357 | "CVSSv2": cvssv2_base_score,
358 | "CVSSv2L": cvssv2_base_score_label,
359 | "CVSSv3": cvssv3_base_score,
360 | "CVSSv3L": cvssv3_base_score_label,
361 | "CVSSv4": cvssv4_base_score,
362 | "CVSSv4L": cvssv4_base_score_label,
363 | "VPR": vpr_score,
364 | "VPRL": vpr_score_label,
365 | "EPSS": epss_score,
366 | "EPSS%": epss_score_label,
367 | }
368 | )
369 |
370 | if scan_summary:
371 | header = summary_data[0].keys()
372 | rows = [x.values() for x in summary_data]
373 | print(tabulate.tabulate(rows, header))
374 |
375 | if plugin_severity:
376 |
377 | default_filter = "@"
378 |
379 | if filter:
380 | expression = jmespath.compile(filter)
381 | else:
382 | expression = jmespath.compile(default_filter)
383 |
384 | plugin_severity_data = expression.search(plugin_severity_data)
385 |
386 | plugin_severity_data.sort(
387 | key=lambda x: (x["Report host name"], -int(x["S"]), int(x["PID"]))
388 | )
389 |
390 | header = plugin_severity_data[0].keys()
391 | rows = [x.values() for x in plugin_severity_data]
392 |
393 | print(tabulate.tabulate(rows, header))
394 |
395 | if scan_file_source:
396 | header = scan_file_source_data[0].keys()
397 | rows = [x.values() for x in scan_file_source_data]
398 | print(tabulate.tabulate(rows, header))
399 |
400 | if policy_summary:
401 | header = policy_summary_data[0].keys()
402 | rows = [x.values() for x in policy_summary_data]
403 | print(tabulate.tabulate(rows, header))
404 |
405 | except FileNotFoundError as e:
406 | print(e.strerror)
407 |
408 | if scan_summary_legend:
409 | print("Legend for scan summary:")
410 | print("File name - nessus file name")
411 | print("Report name - report name for given nessus file name")
412 | print("TH - number of target hosts")
413 | print("SH - number of scanned hosts")
414 | print(
415 | "CC - number of hosts scanned with credentials (Credentialed checks yes in Plugin ID 19506)"
416 | )
417 | print("C - number of plugins with Critical risk factor for whole scan")
418 | print("H - number of plugins with High risk factor for whole scan")
419 | print("M - number of plugins with Medium risk factor for whole scan")
420 | print("L - number of plugins with Low risk factor for whole scan")
421 | print("N - number of plugins with None risk factor for whole scan")
422 |
423 | if plugin_severity_legend:
424 | print("Legend for plugin severity:")
425 | print("File name - nessus file name")
426 | print("Report host name - target name used during scan")
427 | print("PID - Plugin ID reported in scan")
428 | print("S - Severity number (0-4) of plugin")
429 | print("SL - Severity label of plugin (e.g. Critical, High, Medium, Low, None)")
430 | print("RF - Risk factor of plugin (e.g. Critical, High, Medium, Low, None)")
431 | print("CVSSv2 - CVSSv2 base score of plugin")
432 | print("CVSSv2L - CVSSv2 base score label of plugin")
433 | print("CVSSv3 - CVSSv3 base score of plugin")
434 | print("CVSSv3L - CVSSv3 base score label of plugin")
435 | print("CVSSv4 - CVSSv4 base score of plugin")
436 | print("CVSSv4L - CVSSv4 base score label of plugin")
437 | print("VPR - Vulnerability Priority Rating score of plugin")
438 | print("VPRL - Vulnerability Priority Rating label of plugin")
439 | print("EPSS - Exploit Prediction Scoring System score of plugin")
440 | print("EPSS% - Exploit Prediction Scoring System score of plugin in percentage")
441 |
442 |
443 | def main():
444 | name = "nessus file reader (NFR) by LimberDuck"
445 | print("{} {}".format(name, __version__))
446 | cli()
447 |
448 |
449 | if __name__ == "__main__":
450 | main()
451 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nessus file reader (NFR)
2 |
3 | **nessus file reader (NFR) by LimberDuck** (pronounced *ˈlɪm.bɚ dʌk*) is a CLI tool
4 | and python module created to quickly parse nessus files containing the results
5 | of scans performed using Nessus and Tenable.sc by (C) Tenable, Inc. This module will let
6 | you get data through functions grouped into categories like `file`, `scan`, `host`
7 | and `plugin` to get specific information from the provided nessus scan files.
8 |
9 | [](https://pepy.tech/projects/nessus-file-reader) [](https://pepy.tech/projects/nessus-file-reader)
10 | [](https://github.com/LimberDuck/nessus-file-reader/releases)
11 | [](https://github.com/LimberDuck/nessus-file-reader/releases)
12 | [](https://github.com/LimberDuck/nessus-file-reader/blob/master/LICENSE)
13 | [](https://github.com/LimberDuck/nessus-file-reader)
14 | [](https://github.com/LimberDuck/nessus-file-reader)
15 | [](https://github.com/LimberDuck/nessus-file-reader)
16 |
17 |
18 | > [!NOTE]
19 | > **Visit [LimberDuck.org][LimberDuck] to find out more!**
20 |
21 | 
22 |
23 | ## Main features
24 |
25 | * read data from nessus files containing results of scans performed by using Nessus and Tenable.sc by (C) Tenable, Inc.
26 | * use it in CLI to check quickly e.g. quality of your scan, split large scan results
27 | * use it as python module
28 |
29 | > [!TIP]
30 | > Check code [examples].
31 |
32 |
33 | ## Installation
34 |
35 | > [!NOTE]
36 | > It's advisable to use python virtual environment for below instructions. Read more about python virtual environment in [The Hitchhiker’s Guide to Python!](https://docs.python-guide.org/dev/virtualenvs/)
37 | >
38 | >Read about [virtualenvwrapper in The Hitchhiker’s Guide to Python!](https://docs.python-guide.org/dev/virtualenvs/#virtualenvwrapper): [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io) provides a set of commands which makes working with virtual environments much more pleasant.
39 |
40 |
41 | Install **nessus file reader**
42 |
43 | `pip install nessus-file-reader`
44 |
45 | > To upgrade to newer version run:
46 | >
47 | > `pip install -U nessus-file-reader`
48 |
49 |
50 | ## How to
51 |
52 | ### Use nfr in CLI
53 |
54 | 1. Run **nessus file reader**
55 |
56 | `nfr`
57 |
58 | 2. Check help for commands
59 |
60 | `nfr [command] --help` e.g. `nfr file --help`
61 |
62 | #### File command
63 |
64 | Run `nfr file --help` to see options related to nessus file.
65 |
66 | ##### File size
67 |
68 | Check size of given file:
69 | ```commandline
70 | nfr file --size test_files/scan_avrx9t.nessus
71 | nessus file reader (NFR) by LimberDuck 0.4.2
72 | test_files/scan_avrx9t.nessus 2.4 MiB
73 | ```
74 |
75 | more than one file:
76 | ```commandline
77 | nfr file --size test_files/scan_avrx9t.nessus test_files/scan_ihc1js.nessus
78 | nessus file reader (NFR) by LimberDuck 0.4.2
79 | test_files/scan_avrx9t.nessus 2.4 MiB
80 | test_files/scan_ihc1js.nessus 5.0 MiB
81 | ```
82 |
83 | all files in given directory and it's subdirectories:
84 | ```commandline
85 | nfr file --size test_files
86 | nessus file reader (NFR) by LimberDuck 0.4.2
87 | test_files/scan_avrx9t.nessus 2.4 MiB
88 | test_files/scan_ihc1js.nessus 5.0 MiB
89 | test_files/test_subdirectory/scan_ihc1js.nessus 878.3 KiB
90 | ```
91 |
92 | ##### File structure
93 |
94 | Check structure of given file:
95 |
96 | ```commandline
97 | nfr file --structure test_files/scan_avrx9t.nessus
98 | nessus file reader (NFR) by LimberDuck 0.4.2
99 | test_files/scan_avrx9t.nessus
100 | Policy [2/2]
101 | ├── policyName [3/3]
102 | ├── Preferences [2/3]
103 | │ ├── ServerPreferences [1/1]
104 | │ │ ├── preference [54/54]
105 | │ │ │ ├── name [1/1]
106 | │ │ │ └── value [0/1]
107 | │ │ ├── preference [53/54]
108 | ...
109 | │ └── PluginsPreferences [0/1]
110 | │ ├── item [506/506]
111 | │ │ ├── pluginName [6/6]
112 | │ │ ├── pluginId [5/6]
113 | │ │ ├── fullName [4/6]
114 | │ │ ├── preferenceName [3/6]
115 | │ │ ├── preferenceType [2/6]
116 | │ │ ├── preferenceValues [1/6]
117 | │ │ └── selectedValue [0/6]
118 | │ ├── item [505/506]
119 | ...
120 | ├── FamilySelection [1/3]
121 | │ ├── FamilyItem [53/53]
122 | │ │ ├── FamilyName [1/1]
123 | │ │ └── Status [0/1]
124 | │ ├── FamilyItem [52/53]
125 | │ │ ├── FamilyName [1/1]
126 | │ │ └── Status [0/1]
127 | ...
128 | └── IndividualPluginSelection [0/3]
129 | │ ├── PluginItem [6/6]
130 | │ │ ├── PluginId [3/3]
131 | │ │ ├── PluginName [2/3]
132 | │ │ ├── Family [1/3]
133 | │ │ └── Status [0/3]
134 | ...
135 | Report [1/2]
136 | └── ReportHost [0/0]
137 | ├── HostProperties [409/409]
138 | │ ├── tag [354/354]
139 | │ ├── tag [353/354]
140 | ...
141 | ├── ReportItem [408/409]
142 | │ ├── agent [12/12]
143 | │ ├── description [11/12]
144 | │ ├── fname [10/12]
145 | │ ├── plugin_modification_date [9/12]
146 | │ ├── plugin_name [8/12]
147 | │ ├── plugin_publication_date [7/12]
148 | │ ├── plugin_type [6/12]
149 | │ ├── risk_factor [5/12]
150 | │ ├── script_version [4/12]
151 | │ ├── see_also [3/12]
152 | │ ├── solution [2/12]
153 | │ ├── synopsis [1/12]
154 | │ └── plugin_output [0/12]
155 | ...
156 | ```
157 |
158 | Check whole example structure [examples/scan_avrx9t_structure.txt](examples/scan_avrx9t_structure.txt).
159 |
160 | ##### File split
161 |
162 | Split the file with Nessus scan results into smaller files.
163 |
164 | ```commandline
165 | nfr file --split 100 ./directory ./directory2
166 | nessus file reader (NFR) by LimberDuck 0.5.0
167 | ./directory/192_168_8_0_24_3mf2o4.nessus
168 | ./directory/192_168_8_0_24_3mf2o4_part1.nessus
169 | ./directory/192_168_8_0_24_3mf2o4_part2.nessus
170 | ./directory/192_168_8_0_24_3mf2o4_part3.nessus
171 | ./directory/subdirectory/My_Advanced_Scan_for_192_168_8_0_24_rg2ny9.nessus
172 | ./directory/subdirectory/My_Advanced_Scan_for_192_168_8_0_24_rg2ny9_part1.nessus
173 | ./directory2/192_168_8_0_24_3mf2o4.nessus
174 | ./directory2/192_168_8_0_24_3mf2o4_part1.nessus
175 | ./directory2/192_168_8_0_24_3mf2o4_part2.nessus
176 | ./directory2/192_168_8_0_24_3mf2o4_part3.nessus
177 | ```
178 |
179 | #### Scan command
180 |
181 | Run `nfr scan --help` to see options related to content of nessus file on scan level.
182 |
183 | ##### Scan summary
184 |
185 | See scan summary of given file/-s or all files in given directory and it's subdirectories:
186 |
187 | ```commandline
188 | nfr scan --scan-summary scan_avrx9t.nessus
189 | nessus file reader (NFR) by LimberDuck 0.4.2
190 | File name Report name TH SH CC C H M L N
191 | ------------------ ------------ ---- ---- ---- --- --- --- --- ---
192 | scan_avrx9t.nessus test scan 1 1 1 48 182 126 15 38
193 | ```
194 |
195 | ```commandline
196 | nfr scan --scan-summary-legend
197 | nessus file reader (NFR) by LimberDuck 0.4.2
198 | Legend for scan summary:
199 | File name - nessus file name
200 | Report name - report name for given nessus file name
201 | TH - number of target hosts
202 | SH - number of scanned hosts
203 | CC - number of hosts scanned with credentials (Credentialed checks yes in Plugin ID 19506)
204 | C - number of plugins with Critical risk factor for whole scan
205 | H - number of plugins with High risk factor for whole scan
206 | M - number of plugins with Medium risk factor for whole scan
207 | L - number of plugins with Low risk factor for whole scan
208 | N - number of plugins with None risk factor for whole scan
209 | ```
210 |
211 | ##### Plugin severity
212 |
213 | Compare severity scores assigned to plugin like Severity, Risk Factor, CVSSv2, CVSSv3, CVSSv4, VPR, EPSS.
214 |
215 | ```
216 | nfr scan --plugin-severity-legend
217 | nessus file reader (NFR) by LimberDuck 0.6.0
218 | Legend for plugin severity:
219 | File name - nessus file name
220 | Report host name - target name used during scan
221 | PID - Plugin ID reported in scan
222 | S - Severity number (0-4) of plugin
223 | SL - Severity label of plugin (e.g. Critical, High, Medium, Low, None)
224 | RF - Risk factor of plugin (e.g. Critical, High, Medium, Low, None)
225 | CVSSv2 - CVSSv2 base score of plugin
226 | CVSSv2L - CVSSv2 base score label of plugin
227 | CVSSv3 - CVSSv3 base score of plugin
228 | CVSSv3L - CVSSv3 base score label of plugin
229 | CVSSv4 - CVSSv4 base score of plugin
230 | CVSSv4L - CVSSv4 base score label of plugin
231 | VPR - Vulnerability Priority Rating score of plugin
232 | VPRL - Vulnerability Priority Rating label of plugin
233 | EPSS - Exploit Prediction Scoring System score of plugin
234 | EPSS% - Exploit Prediction Scoring System score of plugin in percentage
235 | ```
236 |
237 | Just point the name or path to nessus file with scan results.
238 |
239 | ```
240 | nfr scan --plugin-severity 192_168_1_1_1022nb.nessus
241 | nessus file reader (NFR) by LimberDuck 0.6.0
242 | File name Report host name PID S SL RF CVSSv2 CVSSv2L CVSSv3 CVSSv3L CVSSv4 CVSSv4L VPR VPRL EPSS EPSS%
243 | ------------------------- ------------------ ------ --- ------ ------ -------- --------- -------- --------- -------- --------- ----- ------ ------ -------
244 | 192_168_1_1_1022nb.nessus 192.168.1.10 12217 2 Medium Medium 5 Medium 5.3 Medium
245 | 192_168_1_1_1022nb.nessus 192.168.1.10 42263 2 Medium Medium 5.8 Medium 6.5 Medium
246 | 192_168_1_1_1022nb.nessus 192.168.1.10 50686 2 Medium Medium 5.8 Medium 6.5 Medium 4.9 Medium 0.0596 6.0%
247 | 192_168_1_1_1022nb.nessus 192.168.1.10 10114 1 Low Low 2.1 Low 2.2 Low 0.0037 0.4%
248 | 192_168_1_1_1022nb.nessus 192.168.1.10 10663 1 Low Low 3.3 Low
249 | 192_168_1_1_1022nb.nessus 192.168.1.10 70658 1 Low Low 2.6 Low 3.7 Low 1.4 Low 0.0307 3.1%
250 | 192_168_1_1_1022nb.nessus 192.168.1.10 71049 1 Low Low 2.6 Low
251 | 192_168_1_1_1022nb.nessus 192.168.1.10 153953 1 Low Low 2.6 Low 3.7 Low
252 | 192_168_1_1_1022nb.nessus 192.168.1.10 10107 0 Info None
253 | 192_168_1_1_1022nb.nessus 192.168.1.10 10267 0 Info None
254 | ```
255 |
256 | Use `-f` or `--filter` to check only one Plugin ID among all scan results. Read more about [JMESPath](https://jmespath.org).
257 |
258 | ```
259 | nfr scan --plugin-severity *.nessus -f "[?PID == '50686']"
260 | nessus file reader (NFR) by LimberDuck 0.6.0
261 | File name Report host name PID S SL RF CVSSv2 CVSSv2L CVSSv3 CVSSv3L CVSSv4 CVSSv4L VPR VPRL EPSS EPSS%
262 | --------------------------------- ------------------ ----- --- ------ ------ -------- --------- -------- --------- -------- --------- ----- ------ ------ -------
263 | 192_168_1_1_1022nb-1.nessus 192.168.1.10 50686 2 Medium Medium 5.8 Medium 6.5 Medium 4.9 Medium 0.0596 6.0%
264 | 192_168_1_1_1022nb-2.nessus 192.168.1.10 50686 2 Medium Medium 5.8 Medium 6.5 Medium 4.9 Medium 0.0596 6.0%
265 | ```
266 |
267 | Use `-f` or `--filter` to check only these plugins which have VPR assigned. Read more about [JMESPath](https://jmespath.org).
268 |
269 | ```
270 | nfr scan --plugin-severity 192_168_1_1_1022nb.nessus -f "[?VPR != null]"
271 | nessus file reader (NFR) by LimberDuck 0.6.0
272 | File name Report host name PID S SL RF CVSSv2 CVSSv2L CVSSv3 CVSSv3L CVSSv4 CVSSv4L VPR VPRL EPSS EPSS%
273 | ------------------------- ------------------ ----- --- ------ ------ -------- --------- -------- --------- -------- --------- ----- ------ ------ -------
274 | 192_168_1_1_1022nb.nessus 192.168.1.10 50686 2 Medium Medium 5.8 Medium 6.5 Medium 4.9 Medium 0.0596 6.0%
275 | 192_168_1_1_1022nb.nessus 192.168.1.10 10114 1 Low Low 2.1 Low 2.2 Low 0.0037 0.4%
276 | 192_168_1_1_1022nb.nessus 192.168.1.10 70658 1 Low Low 2.6 Low 3.7 Low 1.4 Low 0.0307 3.1%
277 | ```
278 |
279 | Use `-f` or `--filter` to check only these plugins which have, e.g., CVSSv3 score greater than `4.0`. Read more about [JMESPath](https://jmespath.org).
280 |
281 | ```
282 | nfr scan --plugin-severity 192_168_1_1_1022nb.nessus -f "[?CVSSv3 > '4.0']"
283 | nessus file reader (NFR) by LimberDuck 0.6.0
284 | File name Report host name PID S SL RF CVSSv2 CVSSv2L CVSSv3 CVSSv3L CVSSv4 CVSSv4L VPR VPRL EPSS EPSS%
285 | ------------------------- ------------------ ----- --- ------ ------ -------- --------- -------- --------- -------- --------- ----- ------ ------ -------
286 | 192_168_1_1_1022nb.nessus 192.168.1.10 12217 2 Medium Medium 5 Medium 5.3 Medium
287 | 192_168_1_1_1022nb.nessus 192.168.1.10 42263 2 Medium Medium 5.8 Medium 6.5 Medium
288 | 192_168_1_1_1022nb.nessus 192.168.1.10 50686 2 Medium Medium 5.8 Medium 6.5 Medium 4.9 Medium 0.0596 6.0%
289 | ```
290 |
291 |
292 | ##### Policy scan summary
293 |
294 | See policy scan summary of given file/-s or all files in given directory and it's subdirectories:
295 |
296 | ```commandline
297 | nfr scan --policy-summary scan_ihc1js.nessus scan_avrx9t.nessus
298 | nessus file reader (NFR) by LimberDuck 0.4.2
299 | File name Policy name Max hosts Max checks Checks timeout Plugins number
300 | ------------------ ------------- ----------- ------------ ---------------- ----------------
301 | scan_ihc1js.nessus Advanced Scan 100 5 5 103203
302 | scan_avrx9t.nessus Test 100 5 5 103949
303 |
304 | ```
305 |
306 | ##### Scan file source
307 |
308 | See scan file source like Nessus, Tenable.sc, Tenable.io of given file/-s or all files in given directory and it's subdirectories:
309 |
310 | ```commandline
311 | nfr scan --scan-file-source scan_ihc1js.nessus scan_avrx9t.nessus
312 | nessus file reader (NFR) by LimberDuck 0.4.2
313 | File name Source
314 | ------------------ ----------
315 | scan_ihc1js.nessus Tenable.sc
316 | scan_avrx9t.nessus Nessus
317 | ```
318 |
319 | ### Use nfr as python module
320 |
321 | 1. Import `nessus-file-reader` module.
322 |
323 | ```python
324 | import nessus_file_reader as nfr
325 | ```
326 |
327 | 2. Use `file` functions to get details about provided file e.g. root, file name, file size.
328 |
329 | ```python
330 | import nessus_file_reader as nfr
331 |
332 | nessus_scan_file = './your_nessus_file.nessus'
333 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
334 | file_name = nfr.file.nessus_scan_file_name_with_path(nessus_scan_file)
335 | file_size = nfr.file.nessus_scan_file_size_human(nessus_scan_file)
336 | print(f'File name: {file_name}')
337 | print(f'File size: {file_size}')
338 | ```
339 |
340 | 3. Use `scan` functions to get details about provided scan e.g. report name, number of target/scanned/credentialed hosts, scan time start/end/elapsed and more.
341 |
342 | ```python
343 | import nessus_file_reader as nfr
344 | nessus_scan_file = './your_nessus_file.nessus'
345 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
346 |
347 | report_name = nfr.scan.report_name(root)
348 | number_of_target_hosts = nfr.scan.number_of_target_hosts(root)
349 | number_of_scanned_hosts = nfr.scan.number_of_scanned_hosts(root)
350 | number_of_scanned_hosts_with_credentialed_checks_yes = nfr.scan.number_of_scanned_hosts_with_credentialed_checks_yes(root)
351 | scan_time_start = nfr.scan.scan_time_start(root)
352 | scan_time_end = nfr.scan.scan_time_end(root)
353 | scan_time_elapsed = nfr.scan.scan_time_elapsed(root)
354 | print(f' Report name: {report_name}')
355 | print(f' Number of target/scanned/credentialed hosts: {number_of_target_hosts}/{number_of_scanned_hosts}/{number_of_scanned_hosts_with_credentialed_checks_yes}')
356 | print(f' Scan time START - END (ELAPSED): {scan_time_start} - {scan_time_end} ({scan_time_elapsed})')
357 | ```
358 |
359 | 4. Use `host` functions to get details about hosts from provided scan e.g. report hosts names, operating system, hosts scan time start/end/elapsed, number of Critical/High/Medium/Low/None findings and more.
360 |
361 | ```python
362 | import nessus_file_reader as nfr
363 | nessus_scan_file = './your_nessus_file.nessus'
364 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
365 |
366 | for report_host in nfr.scan.report_hosts(root):
367 | report_host_name = nfr.host.report_host_name(report_host)
368 | report_host_os = nfr.host.detected_os(report_host)
369 | report_host_scan_time_start = nfr.host.host_time_start(report_host)
370 | report_host_scan_time_end = nfr.host.host_time_end(report_host)
371 | report_host_scan_time_elapsed = nfr.host.host_time_elapsed(report_host)
372 | report_host_critical = nfr.host.number_of_plugins_per_risk_factor(report_host, 'Critical')
373 | report_host_high = nfr.host.number_of_plugins_per_risk_factor(report_host, 'High')
374 | report_host_medium = nfr.host.number_of_plugins_per_risk_factor(report_host, 'Medium')
375 | report_host_low = nfr.host.number_of_plugins_per_risk_factor(report_host, 'Low')
376 | report_host_none = nfr.host.number_of_plugins_per_risk_factor(report_host, 'None')
377 | print(f' Report host name: {report_host_name}')
378 | print(f' Report host OS: {report_host_os}')
379 | print(f' Host scan time START - END (ELAPSED): {report_host_scan_time_start} - {report_host_scan_time_end} ({report_host_scan_time_elapsed})')
380 | print(f' Critical/High/Medium/Low/None findings: {report_host_critical}/{report_host_high}/{report_host_medium}/{report_host_low}/{report_host_none}')
381 | ```
382 |
383 | 5. Use `plugin` functions to get details about plugins reported in provided scan e.g. plugins ID, plugins risk factor, plugins name.
384 |
385 | ```python
386 | import nessus_file_reader as nfr
387 | nessus_scan_file = './your_nessus_file.nessus'
388 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
389 |
390 | for report_host in nfr.scan.report_hosts(root):
391 | report_items_per_host = nfr.host.report_items(report_host)
392 | for report_item in report_items_per_host:
393 | plugin_id = int(nfr.plugin.report_item_value(report_item, 'pluginID'))
394 | risk_factor = nfr.plugin.report_item_value(report_item, 'risk_factor')
395 | plugin_name = nfr.plugin.report_item_value(report_item, 'pluginName')
396 | print('\t', plugin_id, ' \t\t\t', risk_factor, ' \t\t\t', plugin_name)
397 | ```
398 |
399 | 6. If you want to get output for interesting you plugin e.g. "Nessus Scan Information" use below function
400 |
401 | ```python
402 | import nessus_file_reader as nfr
403 | nessus_scan_file = './your_nessus_file.nessus'
404 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
405 |
406 | for report_host in nfr.scan.report_hosts(root):
407 | pido_19506 = nfr.plugin.plugin_output(root, report_host, '19506')
408 | print(f'Nessus Scan Information Plugin Output:\n{pido_19506}')
409 | ```
410 |
411 | 7. If you know that interesting you plugin occurs more than ones for particular host e.g. "Netstat Portscanner (SSH)" use below function
412 |
413 | ```python
414 | import nessus_file_reader as nfr
415 | nessus_scan_file = './your_nessus_file.nessus'
416 | root = nfr.file.nessus_scan_file_root_element(nessus_scan_file)
417 |
418 | for report_host in nfr.scan.report_hosts(root):
419 | pidos_14272 = nfr.plugin.plugin_outputs(root, report_host, '14272')
420 | print(f'All findings for Netstat Portscanner (SSH): \n{pidos_14272}')
421 | ```
422 |
423 | ## Meta
424 |
425 | ### Change log
426 |
427 | See [CHANGELOG].
428 |
429 | ### Licence
430 |
431 | GNU GPLv3: [LICENSE].
432 |
433 | ### Authors
434 |
435 | [Damian Krawczyk] created **[nessus file reader (NFR)]** by [LimberDuck].
436 |
437 | [nessus file reader (NFR)]: https://limberduck.org/en/latest/tools/nessus-file-reader
438 | [Damian Krawczyk]: https://damiankrawczyk.com
439 | [LimberDuck]: https://limberduck.org
440 | [CHANGELOG]: https://github.com/LimberDuck/nessus-file-reader/blob/master/CHANGELOG.md
441 | [LICENSE]: https://github.com/LimberDuck/nessus-file-reader/blob/master/LICENSE
442 | [examples]: https://github.com/LimberDuck/nessus-file-reader/tree/master/examples
--------------------------------------------------------------------------------
/nessus_file_reader/utilities.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | nessus file reader (NFR) by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a python module
4 | created to quickly parse nessus files containing the results of scans
5 | performed by using Nessus by (C) Tenable, Inc.
6 | Copyright (C) 2019 Damian Krawczyk
7 |
8 | This program is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation, either version 3 of the License, or
11 | (at your option) any later version.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 | """
21 |
22 | import re
23 | import ipaddress
24 | from xml.etree.ElementTree import parse
25 | import os
26 | import requests
27 | from packaging import version
28 | from nessus_file_reader._version import __version__ as current_version
29 | from nessus_file_reader import __about__
30 |
31 |
32 | def ip_range_split(ip_range):
33 | """
34 | Function takes ip range and resolve it to list of particular IPs
35 | :param ip_range: ip range
36 | :return: list of IPs
37 | """
38 | ip_addresses = []
39 | if re.match(
40 | "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}-\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}", ip_range
41 | ):
42 | address_part = ip_range.split("-")
43 | first_address = ipaddress.IPv4Address(address_part[0])
44 | last_address = ipaddress.IPv4Address(address_part[1])
45 |
46 | while first_address <= last_address:
47 | ip_addresses.append(first_address)
48 | first_address += 1
49 |
50 | elif re.match("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}", ip_range):
51 | ip_network_hosts = ipaddress.ip_network(ip_range).hosts()
52 | ip_network_hosts_list = list(ip_network_hosts)
53 |
54 | for ip in ip_network_hosts_list:
55 | # print(ip)
56 | ip_addresses.append(ip)
57 |
58 | return ip_addresses
59 |
60 |
61 | def nessus_scan_file_structure(file):
62 | """
63 | Function returns the root element for tree of given nessus file with scan results.
64 | :param file: given nessus file
65 |
66 | """
67 |
68 | nessus_scan_file_parsed = parse(file)
69 | root = nessus_scan_file_parsed.getroot()
70 |
71 | root_level = len(root)
72 | root_level_all = len(root)
73 | for child_level_1 in root:
74 | print(f"{child_level_1.tag} [{root_level}/{root_level_all}]")
75 |
76 | child_level_1_len = len(child_level_1)
77 | child_level_1_all = len(child_level_1) - 1
78 | root_level -= 1
79 | # print(f'{root_level}')
80 | for child_level_2 in child_level_1:
81 | child_level_1_len -= 1
82 | # print(f'{root_level} {child_level_1_len}')
83 | if child_level_1_len:
84 | print(
85 | f"├── {child_level_2.tag} [{child_level_1_len}/{child_level_1_all}]"
86 | )
87 | else:
88 | print(
89 | f"└── {child_level_2.tag} [{child_level_1_len}/{child_level_1_all}]"
90 | )
91 |
92 | child_level_2_len = len(child_level_2)
93 | child_level_2_len_all = len(child_level_2) - 1
94 |
95 | for child_level_3 in child_level_2:
96 | child_level_2_len -= 1
97 | child_level_3_len = len(child_level_3)
98 | child_level_3_len_all = len(child_level_3) - 1
99 | # print(f'{root_level} {child_level_1_len} {child_level_2_len}')
100 |
101 | if child_level_1_len and child_level_2_len:
102 | print(
103 | f"│ ├── {child_level_3.tag} [{child_level_2_len}/{child_level_2_len_all}]"
104 | )
105 | elif child_level_1_len and not child_level_2_len:
106 | print(
107 | f"│ └── {child_level_3.tag} [{child_level_2_len}/{child_level_2_len_all}]"
108 | )
109 | elif not root_level and not child_level_1_len and child_level_2_len:
110 | print(
111 | f" ├── {child_level_3.tag} [{child_level_2_len}/{child_level_2_len_all}]"
112 | )
113 | elif not root_level and not child_level_1_len and not child_level_2_len:
114 | print(
115 | f" └── {child_level_3.tag} [{child_level_2_len}/{child_level_2_len_all}]"
116 | )
117 | elif root_level and not child_level_1_len and child_level_2_len:
118 | print(
119 | f"│ ├── {child_level_3.tag} [{child_level_2_len}/{child_level_2_len_all}]"
120 | )
121 | elif root_level and not child_level_1_len and not child_level_2_len:
122 | print(
123 | f"│ └── {child_level_3.tag} [{child_level_2_len}/{child_level_2_len_all}]"
124 | )
125 | else:
126 | print(f"?3 {child_level_3.tag}")
127 |
128 | for child_level_4 in child_level_3:
129 | child_level_3_len -= 1
130 | # print(f'{root_level} {child_level_1_len} {child_level_2_len} {child_level_3_len}')
131 |
132 | if child_level_1_len and child_level_2_len and child_level_3_len:
133 | print(
134 | f"│ │ ├── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
135 | )
136 | elif (
137 | child_level_1_len
138 | and child_level_2_len
139 | and not child_level_3_len
140 | ):
141 | print(
142 | f"│ │ └── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
143 | )
144 | elif (
145 | child_level_1_len
146 | and not child_level_2_len
147 | and child_level_3_len
148 | ):
149 | print(
150 | f"│ ├── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
151 | )
152 | elif (
153 | child_level_1_len
154 | and not child_level_2_len
155 | and not child_level_3_len
156 | ):
157 | print(
158 | f"│ └── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
159 | )
160 | elif (
161 | not root_level
162 | and not child_level_1_len
163 | and child_level_2_len
164 | and child_level_3_len
165 | ):
166 | print(
167 | f" │ ├── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
168 | )
169 | elif (
170 | not root_level
171 | and not child_level_1_len
172 | and child_level_2_len
173 | and not child_level_3_len
174 | ):
175 | print(
176 | f" │ └── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
177 | )
178 | elif (
179 | not root_level
180 | and not child_level_1_len
181 | and not child_level_2_len
182 | and child_level_3_len
183 | ):
184 | print(
185 | f" ├── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
186 | )
187 | elif (
188 | not root_level
189 | and not child_level_1_len
190 | and not child_level_2_len
191 | and not child_level_3_len
192 | ):
193 | print(
194 | f" └── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
195 | )
196 | elif (
197 | root_level
198 | and not child_level_1_len
199 | and child_level_2_len
200 | and child_level_3_len
201 | ):
202 | print(
203 | f"│ │ ├── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
204 | )
205 | elif (
206 | root_level
207 | and not child_level_1_len
208 | and child_level_2_len
209 | and not child_level_3_len
210 | ):
211 | print(
212 | f"│ │ └── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
213 | )
214 | elif (
215 | root_level
216 | and not child_level_1_len
217 | and not child_level_2_len
218 | and child_level_3_len
219 | ):
220 | print(
221 | f"│ ├── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
222 | )
223 | elif (
224 | root_level
225 | and not child_level_1_len
226 | and not child_level_2_len
227 | and not child_level_3_len
228 | ):
229 | print(
230 | f"│ └── {child_level_4.tag} [{child_level_3_len}/{child_level_3_len_all}]"
231 | )
232 | else:
233 | print(f"?4 {child_level_4.tag}")
234 |
235 | child_level_4_len = len(child_level_4)
236 | child_level_4_lena_all = len(child_level_4) - 1
237 | for child_level_5 in child_level_4:
238 | child_level_4_len -= 1
239 | # print(f'{root_level} {child_level_1_len} {child_level_2_len} {child_level_3_len} {child_level_4_len}')
240 |
241 | if (
242 | child_level_1_len
243 | and child_level_2_len
244 | and child_level_3_len
245 | and child_level_4_len
246 | ):
247 | print(
248 | f"│ │ │ ├── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
249 | )
250 | elif (
251 | child_level_1_len
252 | and child_level_2_len
253 | and child_level_3_len
254 | and not child_level_4_len
255 | ):
256 | print(
257 | f"│ │ │ └── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
258 | )
259 | elif (
260 | child_level_1_len
261 | and child_level_2_len
262 | and not child_level_3_len
263 | and child_level_4_len
264 | ):
265 | print(
266 | f"│ │ ├── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
267 | )
268 | elif (
269 | child_level_1_len
270 | and child_level_2_len
271 | and not child_level_3_len
272 | and not child_level_4_len
273 | ):
274 | print(
275 | f"│ │ └── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
276 | )
277 | elif (
278 | child_level_1_len
279 | and not child_level_2_len
280 | and child_level_3_len
281 | and child_level_4_len
282 | ):
283 | print(
284 | f"│ │ ├── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
285 | )
286 | elif (
287 | child_level_1_len
288 | and not child_level_2_len
289 | and child_level_3_len
290 | and not child_level_4_len
291 | ):
292 | print(
293 | f"│ │ └── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
294 | )
295 | elif (
296 | child_level_1_len
297 | and not child_level_2_len
298 | and not child_level_3_len
299 | and child_level_4_len
300 | ):
301 | print(
302 | f"│ ├── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
303 | )
304 | elif (
305 | child_level_1_len
306 | and not child_level_2_len
307 | and not child_level_3_len
308 | and not child_level_4_len
309 | ):
310 | print(
311 | f"│ └── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
312 | )
313 | elif (
314 | not child_level_1_len
315 | and child_level_2_len
316 | and child_level_3_len
317 | and child_level_4_len
318 | ):
319 | print(
320 | f" │ │ ├── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
321 | )
322 | elif (
323 | not child_level_1_len
324 | and child_level_2_len
325 | and child_level_3_len
326 | and not child_level_4_len
327 | ):
328 | print(
329 | f" │ │ └── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
330 | )
331 | elif (
332 | not child_level_1_len
333 | and child_level_2_len
334 | and not child_level_3_len
335 | and child_level_4_len
336 | ):
337 | print(
338 | f" │ ├── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
339 | )
340 | elif (
341 | not child_level_1_len
342 | and child_level_2_len
343 | and not child_level_3_len
344 | and not child_level_4_len
345 | ):
346 | print(
347 | f" │ └── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
348 | )
349 | elif (
350 | not child_level_1_len
351 | and not child_level_2_len
352 | and child_level_3_len
353 | and child_level_4_len
354 | ):
355 | print(
356 | f" │ ├── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
357 | )
358 | elif (
359 | not child_level_1_len
360 | and not child_level_2_len
361 | and child_level_3_len
362 | and not child_level_4_len
363 | ):
364 | print(
365 | f" │ └── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
366 | )
367 | elif (
368 | not child_level_1_len
369 | and not child_level_2_len
370 | and not child_level_3_len
371 | and child_level_4_len
372 | ):
373 | print(
374 | f" ├── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
375 | )
376 | elif (
377 | not child_level_1_len
378 | and not child_level_2_len
379 | and not child_level_3_len
380 | and not child_level_4_len
381 | ):
382 | print(
383 | f" └── {child_level_5.tag} [{child_level_4_len}/{child_level_4_lena_all}]"
384 | )
385 | else:
386 | print(f"?5 {child_level_5.tag} [{child_level_4_len}]")
387 |
388 |
389 | def nessus_scan_file_split(input_file_path: str, batch_size: int) -> None:
390 | """
391 | Splits a .nessus XML file into multiple files, each containing a specified number of ReportHost entries.
392 | Preserves the original XML formatting, including entities like ' and ".
393 |
394 | :param input_file_path: Path to the input .nessus file.
395 | :param output_file_prefix: Prefix for the output files.
396 | :param batch_size: Number of ReportHost entries per split file.
397 | """
398 | with open(input_file_path, "r", encoding="utf-8") as file:
399 | xml_content = file.read()
400 |
401 | # Extract the Policy section
402 | policy_start = xml_content.find("")
403 | policy_end = xml_content.find("") + len("")
404 | policy_element = xml_content[policy_start:policy_end]
405 |
406 | # Extract the Report section
407 | report_start = xml_content.find("")
409 | report_element = xml_content[report_start:report_end]
410 | report_hosts = report_element.split("'
417 |
418 | # Split ReportHost elements into batches
419 | for i in range(0, len(report_hosts), batch_size):
420 | batch = report_hosts[i : i + batch_size]
421 |
422 | # Construct the new XML content
423 | new_xml = xml_content[:report_start]
424 | new_xml += report_name_line + "\n"
425 | new_xml += "".join("\n\n"
427 |
428 | # Insert the Policy section
429 | new_xml = new_xml[:policy_start] + policy_element + new_xml[policy_end:]
430 |
431 | # Write the new XML content to a file
432 | output_file_prefix = os.path.splitext(input_file_path)[0]
433 | output_file = f"{output_file_prefix}_part{i // batch_size + 1}.nessus"
434 | print(output_file)
435 | with open(output_file, "w", encoding="utf-8") as out_file:
436 | out_file.write(new_xml)
437 |
438 |
439 | def check_for_update():
440 |
441 | PACKAGE_NAME = __about__.__package_name__
442 |
443 | try:
444 | response = requests.get(
445 | f"https://pypi.org/pypi/{PACKAGE_NAME}/json", timeout=1.5
446 | )
447 | response.raise_for_status()
448 | latest = response.json()["info"]["version"]
449 | read_more = (
450 | f"> Read more:\n"
451 | f"> https://limberduck.org/en/latest/tools/{PACKAGE_NAME}\n"
452 | f"> https://github.com/LimberDuck/{PACKAGE_NAME}\n"
453 | f"> https://github.com/LimberDuck/{PACKAGE_NAME}/releases"
454 | )
455 | if version.parse(latest) > version.parse(current_version):
456 | print(
457 | f"\n> A new version of {PACKAGE_NAME} is available: {latest} (you have {current_version})"
458 | )
459 | print(f"> Update with: pip install -U {PACKAGE_NAME}\n")
460 | print(read_more)
461 | elif version.parse(latest) == version.parse(current_version):
462 | print(
463 | f"\n> You are using the latest version of {PACKAGE_NAME}: {current_version}\n"
464 | )
465 | print(read_more)
466 | else:
467 | print(
468 | f"\n> You are using a pre-release version of {PACKAGE_NAME}: {current_version}"
469 | )
470 | print(f"> Latest released version of {PACKAGE_NAME}: {latest}\n")
471 | print(read_more)
472 | except requests.exceptions.ConnectionError as e:
473 | print("> Could not check for updates: Connection error.\n")
474 | print(e)
475 | except Exception as e:
476 | print("> Could not check for updates:\n")
477 | print(e)
478 |
--------------------------------------------------------------------------------
/nessus_file_reader/scan/scan.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | nessus file reader (NFR) by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a python module
4 | created to quickly parse nessus files containing the results of scans
5 | performed by using Nessus by (C) Tenable, Inc.
6 | Copyright (C) 2019 Damian Krawczyk
7 |
8 | This program is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation, either version 3 of the License, or
11 | (at your option) any later version.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 | """
21 |
22 | import re
23 | import datetime
24 | from nessus_file_reader.plugin import plugin
25 | from nessus_file_reader import utilities
26 |
27 |
28 | def report_name(root):
29 | """
30 | Function returns scan report name.
31 | :param root: root element of scan file tree
32 | :return: scan report name
33 | """
34 | name = root.find("Report").get("name")
35 | return name
36 |
37 |
38 | def policy_name(root):
39 | """
40 | Function returns policy name used during scan.
41 | :param root: root element of scan file tree
42 | :return: policy name
43 | """
44 | if root.find("Policy"):
45 | name = root.find("Policy").find("policyName")
46 | if name is not None:
47 | name = name.text
48 | else:
49 | name = None
50 | else:
51 | name = None
52 | return name
53 |
54 |
55 | def server_preference_value(root, preference_name):
56 | """
57 | Function returns value for given server preference.
58 | :param root: root element of scan file tree
59 | :param preference_name: preference name
60 | :return:
61 | preference value - if preference exist
62 | None - if preference does not exist
63 | """
64 |
65 | if root.find("Policy"):
66 | status = 0
67 | preference_value = None
68 | for preference in root.find("Policy/Preferences/ServerPreferences").findall(
69 | "preference"
70 | ):
71 | preference_name_in_report = preference.findtext("name")
72 | if preference_name_in_report == preference_name:
73 | preference_value = preference.findtext("value")
74 | status = 1
75 | if status == 0:
76 | preference_value = None
77 | else:
78 | preference_value = None
79 |
80 | return preference_value
81 |
82 |
83 | def scan_file_source(root):
84 | """
85 | Function returns information about source of file, Tenable.sc Tenable.io or Nessus.
86 | :param root: root element of scan file tree
87 | :return:
88 | 'Tenable.sc' if Tenable.sc is source of nessus file
89 | 'Tenable.io' if Tenable.io is source of nessus file
90 | 'Nessus' if Nessus is source of nessus file
91 | """
92 | tenableio_site_id = server_preference_value(root, "tenableio.site_id")
93 | sc_version = server_preference_value(root, "sc_version")
94 |
95 | if tenableio_site_id is not None:
96 | source = "Tenable.io"
97 | elif sc_version is not None:
98 | source = "Tenable.sc"
99 | else:
100 | source = "Nessus"
101 | return source
102 |
103 |
104 | def policy_max_hosts(root):
105 | """
106 | Function returns Max simultaneous checks per host value specified in policy used during scan.
107 | :param root: root element of scan file tree
108 | :return: max host value or None
109 | """
110 | max_hosts = server_preference_value(root, "max_hosts")
111 | return max_hosts
112 |
113 |
114 | def policy_max_checks(root):
115 | """
116 | Function returns Max simultaneous hosts per scan value specified in policy used during scan.
117 | :param root: root element of scan file tree
118 | :return: max checks value or None
119 | """
120 | max_checks = server_preference_value(root, "max_checks")
121 | return max_checks
122 |
123 |
124 | def policy_checks_read_timeout(root):
125 | """
126 | Function returns Network timeout (in seconds) value specified in policy used during scan.
127 | :param root: root element of scan file tree
128 | :return: network timeout value or None
129 | """
130 | checks_read_timeout = server_preference_value(root, "checks_read_timeout")
131 | return checks_read_timeout
132 |
133 |
134 | def reverse_lookup(root):
135 | """
136 | Function returns information if option Settings > Report > Output > 'Designate hosts by their DNS name' has been
137 | turned on in policy used during scan.
138 | :param root: root element of scan file tree
139 | :return:
140 | 'yes' if reverse_lookup has been enabled
141 | 'no' if reverse_lookup has not been enabled
142 | """
143 | reverse_lookup_value = server_preference_value(root, "reverse_lookup")
144 | return reverse_lookup_value
145 |
146 |
147 | def plugin_set(root):
148 | """
149 | Function returns list of plugins selected in policy used during scan.
150 | :param root: root element of scan file tree
151 | :return: list of plugins selected in policy or None
152 | """
153 | plugin_set_list = server_preference_value(root, "plugin_set")
154 | if plugin_set_list:
155 | plugin_set_list = plugin_set_list[:-1].split(";")
156 | else:
157 | plugin_set_list = None
158 | return plugin_set_list
159 |
160 |
161 | def plugin_set_number(root):
162 | """
163 | Function returns number of plugins selected in policy used during scan.
164 | :param root: root element of scan file tree
165 | :return: number of plugins selected in policy
166 | """
167 | plugin_set_list = plugin_set(root)
168 | if plugin_set_list is not None:
169 | plugin_set_len = len(plugin_set_list)
170 | else:
171 | plugin_set_len = None
172 | return plugin_set_len
173 |
174 |
175 | def plugin_preference_value(root, full_preference_name):
176 | """
177 | Function returns value for given full preference name of plugin.
178 | :param root: root element of scan file tree
179 | :param full_preference_name: full preference name of plugin
180 | :return: preference value or None
181 | """
182 | preference = root.find(
183 | "Policy/Preferences/PluginsPreferences/item/[fullName='"
184 | + full_preference_name
185 | + "']/selectedValue"
186 | )
187 | if preference is not None:
188 | preference_value = preference.text
189 | else:
190 | preference_value = None
191 | return preference_value
192 |
193 |
194 | def policy_db_sid(root):
195 | """
196 | Function returns Database SID specified in policy used during scan.
197 | :param root: root element of scan file tree
198 | :return: Database SID name or None
199 | """
200 | sid = plugin_preference_value(root, "Database settings[entry]:Database SID :")
201 | return sid
202 |
203 |
204 | def policy_db_port(root):
205 | """
206 | Function returns Database port specified in policy used during scan.
207 | :param root: root element of scan file tree
208 | :return: Database port or None
209 | """
210 | port = plugin_preference_value(
211 | root, "Database settings[entry]:Database port to use :"
212 | )
213 | return port
214 |
215 |
216 | def policy_login_specified(root):
217 | """
218 | Function returns login specified in policy used during scan.
219 | Currently covered: smb, ssh, database, VMware vCenter SOAP API
220 | :param root: root element of scan file tree
221 | :return: login name or None
222 | """
223 |
224 | login_vmware_vcenter_soap_api = plugin_preference_value(
225 | root, "VMware vCenter SOAP API Settings[entry]:VMware vCenter user name :"
226 | )
227 | login_database = plugin_preference_value(root, "Database settings[entry]:Login :")
228 | login_smb = plugin_preference_value(
229 | root, "Login configurations[entry]:SMB account :"
230 | )
231 | login_ssh = plugin_preference_value(root, "SSH settings[entry]:SSH user name :")
232 |
233 | if login_vmware_vcenter_soap_api:
234 | login_specified = login_vmware_vcenter_soap_api
235 | elif login_database:
236 | login_specified = login_database
237 | elif login_smb:
238 | domain_smb_domain = plugin_preference_value(
239 | root, "Login configurations[entry]:SMB domain (optional) :"
240 | )
241 | if domain_smb_domain:
242 | login_specified = domain_smb_domain + "\\" + login_smb
243 | else:
244 | login_specified = login_smb
245 | elif login_ssh:
246 | login_specified = login_ssh
247 | else:
248 | login_specified = None
249 |
250 | return login_specified
251 |
252 |
253 | def list_of_target_hosts_raw(root):
254 | """
255 | Function returns list of target hosts specified in scan.
256 | :param root: root element of scan file tree
257 | :return: list of targets
258 | """
259 | target_hosts = root.find(
260 | "Policy/Preferences/ServerPreferences/preference/[name='TARGET']/value"
261 | )
262 | if target_hosts is not None:
263 | target_hosts = target_hosts.text
264 | target_hosts_splitted = target_hosts.split(",")
265 | target_hosts_final_list = [element.lower() for element in target_hosts_splitted]
266 | else:
267 | target_hosts_final_list = None
268 | return target_hosts_final_list
269 |
270 |
271 | def list_of_target_hosts(root):
272 | """
273 | Function returns list of target hosts specified in scan.
274 | If nessus files comes from Tenable.sc and has [IP] in target it's removed
275 | If nessus files comes from Tenable.sc and has IP range in target it's resolved to particular IP addresses
276 | :param root: root element of scan file tree
277 | :return: list of targets
278 | """
279 | target_hosts = root.find(
280 | "Policy/Preferences/ServerPreferences/preference/[name='TARGET']/value"
281 | )
282 | target_hosts_final_list = []
283 | if target_hosts is not None:
284 | target_hosts = target_hosts.text
285 | target_hosts_splitted = target_hosts.split(",")
286 | target_hosts_splitted_lower = [
287 | element.lower() for element in target_hosts_splitted
288 | ]
289 | # if nessus file comes from Tenable.sc remove '[ip]' from target
290 | target_hosts_splitted_lower_clear = [
291 | element.split("[", 1)[0] for element in target_hosts_splitted_lower
292 | ]
293 | # if nessus file comes from Tenable.sc convert IP ranges in target into separate IP addresses
294 | for target in target_hosts_splitted_lower_clear:
295 | if re.match(
296 | "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}-\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}",
297 | target,
298 | ):
299 | address_range = utilities.ip_range_split(target)
300 | for address in address_range:
301 | target_hosts_final_list.append(str(address))
302 | elif re.match("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}", target):
303 | address_range = utilities.ip_range_split(target)
304 | for address in address_range:
305 | target_hosts_final_list.append(str(address))
306 | else:
307 | target_hosts_final_list.append(target)
308 | else:
309 | target_hosts_final_list = None
310 | return target_hosts_final_list
311 |
312 |
313 | def list_of_target_hosts_sc_fqdn_ip(root):
314 | """
315 | Function returns list of target hosts as dictionary of fqdn and ip. Works only if nessus file comes from Tenable.sc.
316 | :param root: root element of scan file tree
317 | :return: dictionary of fqdn and ip
318 | """
319 | target_list = []
320 | target_hosts = root.find(
321 | "Policy/Preferences/ServerPreferences/preference/[name='TARGET']/value"
322 | )
323 | if target_hosts is not None:
324 | target_hosts = target_hosts.text
325 | target_hosts_splitted = target_hosts.split(",")
326 | for target in target_hosts_splitted:
327 | target_splitted = target[:-1].split("[")
328 | if len(target_splitted) == 2:
329 | target_list.append(
330 | {"target_fqdn": target_splitted[0], "target_ip": target_splitted[1]}
331 | )
332 | else:
333 | target_list = None
334 | return target_list
335 |
336 |
337 | def report_hosts(root):
338 | """
339 | Function returns list of report hosts available in given file.
340 | :param root: root element of scan file tree
341 | :return: list report hosts
342 | """
343 | hosts = root.find("Report").findall("ReportHost")
344 | return hosts
345 |
346 |
347 | def list_of_scanned_hosts(root):
348 | """
349 | Functions returns list of names of scanned hosts.
350 | :param root: root element of scan file tree
351 | :return: list of names of scanned hosts
352 | """
353 | report_hosts_names = list()
354 | for report_host in report_hosts(root):
355 | report_host_name = report_host.get("name")
356 | report_hosts_names.append(report_host_name)
357 | return report_hosts_names
358 |
359 |
360 | def list_of_not_scanned_hosts(root):
361 | """
362 | Function returns list of not scanned hosts.
363 | :param root: root element of scan file tree
364 | :return: list of not scanned hosts or empty list
365 | """
366 | target_hosts = list_of_target_hosts(root)
367 | scanned_hosts = list_of_scanned_hosts(root)
368 | if target_hosts:
369 | not_scanned_hosts = list(set(target_hosts) - set(scanned_hosts))
370 | else:
371 | not_scanned_hosts = None
372 | return not_scanned_hosts
373 |
374 |
375 | def number_of_target_hosts(root):
376 | """
377 | Function returns number of target hosts.
378 | :param root: root element of scan file tree
379 | :return: number of target hosts
380 | """
381 | target_hosts = list_of_target_hosts(root)
382 | if target_hosts is not None:
383 | number_of_targets = len(target_hosts)
384 | else:
385 | number_of_targets = None
386 | return number_of_targets
387 |
388 |
389 | def number_of_target_hosts_without_duplicates(root):
390 | """
391 | Function returns number of actual target hosts (without duplicated entries).
392 | :param root: root element of scan file tree
393 | :return: number of actual target hosts
394 | """
395 | target_hosts = list_of_target_hosts(root)
396 |
397 | if target_hosts:
398 | actual_number_of_targets = len(set(target_hosts))
399 | else:
400 | actual_number_of_targets = None
401 |
402 | return actual_number_of_targets
403 |
404 |
405 | def number_of_scanned_hosts(root):
406 | """
407 | Function returns number of scanned hosts.
408 | :param root: root element of scan file tree
409 | :return: number of scanned hosts
410 | """
411 | number = len(list_of_scanned_hosts(root))
412 | return number
413 |
414 |
415 | def number_of_not_scanned_hosts(root):
416 | """
417 | Function returns number of not scanned hosts.
418 | :param root: root element of scan file tree
419 | :return: number of not scanned hosts
420 | """
421 | not_scanned_hosts = list_of_not_scanned_hosts(root)
422 | if not_scanned_hosts:
423 | number_of_not_scanned_hosts = len(not_scanned_hosts)
424 | else:
425 | number_of_not_scanned_hosts = None
426 | return number_of_not_scanned_hosts
427 |
428 |
429 | def number_of_scanned_hosts_with_credentialed_checks_yes(root):
430 | """
431 | Function returns number of scanned hosts with credentialed checks yes.
432 | :param root: root element of scan file tree
433 | :return: number of scanned hosts with credentialed checks yes
434 | """
435 | number_of_report_hosts_with_credentialed_checks = 0
436 |
437 | for report_host in report_hosts(root):
438 | pido_19506 = plugin.plugin_output(root, report_host, "19506")
439 | if (
440 | "no output recorded" in pido_19506
441 | or "check Audit Trail" in pido_19506
442 | or "not enabled." in pido_19506
443 | or "info about used plugins not available" in pido_19506
444 | ):
445 | number_of_report_hosts_with_credentialed_checks = None
446 | else:
447 | for line in pido_19506.split("\n"):
448 | if re.findall("Credentialed checks :", line):
449 | if re.findall("yes", line):
450 | number_of_report_hosts_with_credentialed_checks += 1
451 |
452 | return number_of_report_hosts_with_credentialed_checks
453 |
454 |
455 | def number_of_scanned_dbs_with_credentialed_checks_yes(root):
456 | """
457 | Function returns number of scanned dbs with credentialed checks yes.
458 | :param root: root element of scan file tree
459 | :return: number of scanned dbs with credentialed checks yes
460 | """
461 | number_of_scanned_dbs_with_credentialed_checks = 0
462 |
463 | for report_host in report_hosts(root):
464 |
465 | # "91825: Oracle DB Login Possible"
466 | pido_91825 = plugin.plugin_output(root, report_host, "91825")
467 | if re.findall(
468 | "Credentialed checks have been enabled for Oracle RDBMS server", pido_91825
469 | ):
470 | number_of_scanned_dbs_with_credentialed_checks += 1
471 |
472 | # "91827: Oracle DB Login Possible"
473 | pido_91827 = plugin.plugin_output(root, report_host, "91827")
474 | if re.findall(
475 | "Credentialed checks have been enabled for MSSQL server", pido_91827
476 | ):
477 | number_of_scanned_dbs_with_credentialed_checks += 1
478 |
479 | return number_of_scanned_dbs_with_credentialed_checks
480 |
481 |
482 | def scan_time_start(root):
483 | """
484 | Function returns scan time start.
485 | :param root: root element of scan file tree
486 | :return: date and time when scan has been started
487 | """
488 |
489 | min_date_start_check = root.find(
490 | "Report/ReportHost[1]/HostProperties/tag/[@name='HOST_START']"
491 | )
492 |
493 | if min_date_start_check is not None:
494 | min_date_start = min_date_start_check.text
495 | min_date_start_parsed = datetime.datetime.strptime(
496 | min_date_start, "%a %b %d %H:%M:%S %Y"
497 | )
498 |
499 | max_date_end = root.find(
500 | "Report/ReportHost[1]/HostProperties/tag/[@name='HOST_END']"
501 | ).text
502 | max_date_end_parsed = datetime.datetime.strptime(
503 | max_date_end, "%a %b %d %H:%M:%S %Y"
504 | )
505 |
506 | for reportHost in root.find("Report").findall("ReportHost"):
507 | host_end_time_find = reportHost[0].find("tag/[@name='HOST_END']")
508 | if host_end_time_find is not None:
509 | host_end_time = host_end_time_find.text
510 | host_start_time = reportHost[0].find("tag/[@name='HOST_START']").text
511 |
512 | host_end_time_parsed = datetime.datetime.strptime(
513 | host_end_time, "%a %b %d %H:%M:%S %Y"
514 | )
515 | host_start_time_parsed = datetime.datetime.strptime(
516 | host_start_time, "%a %b %d %H:%M:%S %Y"
517 | )
518 |
519 | if min_date_start_parsed > host_start_time_parsed:
520 | min_date_start_parsed = host_start_time_parsed
521 |
522 | if max_date_end_parsed < host_end_time_parsed:
523 | max_date_end_parsed = host_end_time_parsed
524 | else:
525 | min_date_start_parsed = None
526 | return min_date_start_parsed
527 |
528 |
529 | def scan_time_end(root):
530 | """
531 | Function returns scan time end.
532 | :param root: root element of scan file tree
533 | :return: date and time when scan has been ended
534 | """
535 |
536 | min_date_start_check = root.find(
537 | "Report/ReportHost[1]/HostProperties/tag/[@name='HOST_START']"
538 | )
539 |
540 | if min_date_start_check is not None:
541 | min_date_start = min_date_start_check.text
542 | min_date_start_parsed = datetime.datetime.strptime(
543 | min_date_start, "%a %b %d %H:%M:%S %Y"
544 | )
545 |
546 | max_date_end = root.find(
547 | "Report/ReportHost[1]/HostProperties/tag/[@name='HOST_END']"
548 | ).text
549 | max_date_end_parsed = datetime.datetime.strptime(
550 | max_date_end, "%a %b %d %H:%M:%S %Y"
551 | )
552 |
553 | for reportHost in root.find("Report").findall("ReportHost"):
554 | host_end_time_find = reportHost[0].find("tag/[@name='HOST_END']")
555 | if host_end_time_find is not None:
556 | host_end_time = host_end_time_find.text
557 | host_start_time = reportHost[0].find("tag/[@name='HOST_START']").text
558 |
559 | host_end_time_parsed = datetime.datetime.strptime(
560 | host_end_time, "%a %b %d %H:%M:%S %Y"
561 | )
562 | host_start_time_parsed = datetime.datetime.strptime(
563 | host_start_time, "%a %b %d %H:%M:%S %Y"
564 | )
565 |
566 | if min_date_start_parsed > host_start_time_parsed:
567 | min_date_start_parsed = host_start_time_parsed
568 |
569 | if max_date_end_parsed < host_end_time_parsed:
570 | max_date_end_parsed = host_end_time_parsed
571 | else:
572 | max_date_end_parsed = None
573 | return max_date_end_parsed
574 |
575 |
576 | def scan_time_elapsed(root):
577 | """
578 | Function returns scan time elapsed in format HH:MM:SS
579 | :param root: root element of scan file tree
580 | :return: scan time elapsed in format HH:MM:SS
581 | """
582 |
583 | min_date_start_check = root.find(
584 | "Report/ReportHost[1]/HostProperties/tag/[@name='HOST_START']"
585 | )
586 |
587 | if min_date_start_check is not None:
588 | min_date_start = min_date_start_check.text
589 | min_date_start_parsed = datetime.datetime.strptime(
590 | min_date_start, "%a %b %d %H:%M:%S %Y"
591 | )
592 | max_date_end = root.find(
593 | "Report/ReportHost[1]/HostProperties/tag/[@name='HOST_END']"
594 | ).text
595 | max_date_end_parsed = datetime.datetime.strptime(
596 | max_date_end, "%a %b %d %H:%M:%S %Y"
597 | )
598 |
599 | for reportHost in root.find("Report").findall("ReportHost"):
600 | host_end_time_find = reportHost[0].find("tag/[@name='HOST_END']")
601 | if host_end_time_find is not None:
602 | host_end_time = host_end_time_find.text
603 | host_start_time = reportHost[0].find("tag/[@name='HOST_START']").text
604 |
605 | host_end_time_parsed = datetime.datetime.strptime(
606 | host_end_time, "%a %b %d %H:%M:%S %Y"
607 | )
608 | host_start_time_parsed = datetime.datetime.strptime(
609 | host_start_time, "%a %b %d %H:%M:%S %Y"
610 | )
611 |
612 | if min_date_start_parsed > host_start_time_parsed:
613 | min_date_start_parsed = host_start_time_parsed
614 |
615 | if max_date_end_parsed < host_end_time_parsed:
616 | max_date_end_parsed = host_end_time_parsed
617 |
618 | whole_scan_duration = max_date_end_parsed - min_date_start_parsed
619 | whole_scan_duration_parsed = str(whole_scan_duration)
620 |
621 | else:
622 | whole_scan_duration_parsed = None
623 | return whole_scan_duration_parsed
624 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
--------------------------------------------------------------------------------