├── nexpose ├── __init__.py ├── nexpose_criteria_constants.py ├── nexpose_status.py ├── nexpose_backup.py ├── nexpose_userauthenticator.py ├── python_utils.py ├── json_utils.py ├── nexpose_node.py ├── nexpose_privileges.py ├── xml_utils.py ├── nexpose_assetgroup.py ├── nexpose_criteria_operators.py ├── nexpose_assetfilter.py ├── nexpose_enginepool.py ├── nexpose_engine.py ├── nexpose_vulnerability.py ├── nexpose_report.py ├── nexpose_discoveryconnection.py ├── nexpose_vulnerabilityexception.py ├── nexpose_role.py ├── nexpose_tag.py ├── nexpose_ticket.py ├── nexpose_criteria.py ├── nexpose_asset.py ├── nexpose_scansummary.py ├── nexpose_sharedcredential.py ├── nexpose_user.py ├── nexpose_criteria_fields.py ├── nexpose_site.py └── nexpose_credential.py ├── MANIFEST.in ├── requirements.txt ├── docs ├── modules.rst ├── index.rst ├── nexpose.rst ├── Makefile ├── make.bat └── conf.py ├── requirements_dev.txt ├── setup.cfg ├── requirements_test.txt ├── demo ├── demo.cfg.default └── sslfix.py ├── .travis.yml ├── test_fixtures ├── ReportHistoryResponse.xml ├── UserAuthenticatorListingResponse.xml ├── xml_fixtures.py ├── ReportListingResponse.xml ├── custom_tag_example.json ├── ReportConfigResponse_Samples.xml ├── data-scan_complete-assets_Nexpose6.json └── default_tags.json ├── contributors.md ├── .flake8 ├── tests ├── __init__.py ├── load_unittest.py ├── test_NexposeNode.py ├── test_LoadFixture.py ├── LoadFixture.py ├── test_NexposeUserAuthenticatorSummary.py ├── NexposeSessionSpy.py ├── test_NexposeReportConfigurationSummary.py ├── test_NexposeSession.py ├── test_NexposeTag.py └── test_NexposeVulnerability.py ├── setup.py ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.md ├── LICENSE ├── .gitignore ├── README.md ├── README.rst └── CHANGELOG.md /nexpose/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml>=3.6.0 2 | future>=0.16.0 -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | nexpose 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | nexpose 8 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | lxml>=3.6.0 2 | py>=1.4.31 3 | pytest>=2.9.2 4 | Sphinx>=1.4.5 5 | future>=0.16.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # Indicate that this library can be packaged universally for python 2 and 3. 3 | universal=1 -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | flake8>=3.4.1; python_version >= '2.7' 2 | future>=0.16.0 3 | lxml>=3.6.0 4 | py>=1.4.31 5 | pytest>=2.9.2 6 | unittest2>=1.1.0; python_version == '2.6' 7 | -------------------------------------------------------------------------------- /demo/demo.cfg.default: -------------------------------------------------------------------------------- 1 | # This file contains the Nexpose login information used by "run_demo.py". 2 | # Lines started with a #-character are ignored. 3 | # Copy "demo.cfg.default" to "demo.cfg". 4 | # Then edit "demo.cfg" as needed, keeping the format intact. 5 | # The username and/or password should not contain spaces or tabs. 6 | localhost 3780 nxadmin nxpassword 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | branches: 3 | only: 4 | - master 5 | language: python 6 | cache: pip 7 | python: 8 | - 2.6 9 | - 2.7 10 | - 3.4 11 | - 3.5 12 | - 3.6 13 | install: 14 | - pip install -r requirements.txt 15 | - pip install -r requirements_test.txt 16 | script: 17 | - if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then flake8; fi 18 | - py.test tests 19 | -------------------------------------------------------------------------------- /test_fixtures/ReportHistoryResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test_fixtures/UserAuthenticatorListingResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /contributors.md: -------------------------------------------------------------------------------- 1 | # Project Contributors 2 | 3 | Original authors of the Nexpose client library for Python (Davinsi Labs): 4 | 5 | - [@dLabsPeterL](https://github.com/dLabsPeterL) 6 | - [@kbossaert](https://github.com/kbossaert) 7 | 8 | Rapid7 authors contributing to initial public open source release: 9 | 10 | - [@gschneider-r7](https://github.com/gschneider-r7) 11 | - [@bglass-r7](https://github.com/bglass-r7) 12 | 13 | Additional contributors: 14 | 15 | -------------------------------------------------------------------------------- /test_fixtures/xml_fixtures.py: -------------------------------------------------------------------------------- 1 | # TODO: decide wether or not to use this class (currently unused) 2 | 3 | class NexposeXmlFixtures: 4 | LoginRequest = '' 5 | LoginResponse = '' 6 | LogoutRequest = '' 7 | LogoutResponse = '' 8 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .cache, 4 | .git, 5 | .github, 6 | *.txt, 7 | __pycache__, 8 | build, 9 | demo, 10 | dist, 11 | docs, 12 | test_fixtures 13 | 14 | ignore= 15 | # too many long lines to worry about for now, revisit later 16 | E501, 17 | # lots of unused imports, revisit later 18 | F401, 19 | # lambda assignment, revisit later 20 | E731, 21 | # need to clean up all imports, revisit later 22 | E402, 23 | F403, 24 | F405 25 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from os import path 5 | from .load_unittest import * 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | 9 | NexposeTestSuite = unittest.TestLoader().discover(path.dirname(__file__), "test_*.py") 10 | 11 | 12 | def main(): 13 | runner = unittest.TextTestRunner(verbosity=2) 14 | runner.run(NexposeTestSuite) 15 | -------------------------------------------------------------------------------- /nexpose/nexpose_criteria_constants.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from future import standard_library 6 | standard_library.install_aliases() 7 | 8 | 9 | class NexposeCriteriaConstant(object): 10 | class __metaclass__(type): 11 | @property 12 | def Name(cls): 13 | return cls.__name__ 14 | 15 | def __str__(cls): 16 | return cls.Name 17 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. nexpose-client-python documentation master file, created by 2 | sphinx-quickstart on Fri Jul 22 22:46:51 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to nexpose-client-python's documentation! 7 | ================================================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | .. automodule:: nexpose 15 | 16 | .. autoclass:: NexposeSessionBase 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | -------------------------------------------------------------------------------- /test_fixtures/ReportListingResponse.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/load_unittest.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | import sys 5 | from future import standard_library 6 | standard_library.install_aliases() 7 | 8 | 9 | def _assertIsInstance(self, obj, class_or_type_or_tuple): 10 | self.assertTrue(isinstance(obj, class_or_type_or_tuple)) 11 | 12 | 13 | if sys.version_info[0] == 2 and sys.version_info[1] <= 6: 14 | try: 15 | import unittest2 as unittest 16 | except ImportError: 17 | import unittest 18 | 19 | unittest.TestCase.assertIsInstance = _assertIsInstance 20 | else: 21 | import unittest 22 | -------------------------------------------------------------------------------- /tests/test_NexposeNode.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from .load_unittest import unittest 5 | from .LoadFixture import LoadFixture, JSON 6 | from nexpose.nexpose_node import Node 7 | from future import standard_library 8 | standard_library.install_aliases() 9 | 10 | 11 | class NexposeNodeTestCase(unittest.TestCase): 12 | def testCreateFromJSON(self): 13 | fixture = LoadFixture('data-scan_complete-assets_Nexpose6.json') 14 | records = fixture['records'] 15 | self.assertEqual(len(records), 3) 16 | 17 | record_empty = Node.CreateFromJSON(records[2]) # noqa F841 18 | -------------------------------------------------------------------------------- /test_fixtures/custom_tag_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "asset_ids":[ 3 | 1 4 | ], 5 | "url":"https://localhost/api/2.0/tags/12", 6 | "tag_id":12, 7 | "tag_name":"example", 8 | "attributes":[ 9 | { 10 | "tag_attribute_value":"Nexpose", 11 | "tag_attribute_name":"SOURCE", 12 | "tag_attribute_id":61 13 | }, 14 | { 15 | "tag_attribute_value":"#de7200", 16 | "tag_attribute_name":"COLOR", 17 | "tag_attribute_id":63 18 | } 19 | ], 20 | "tag_config":{ 21 | "asset_group_ids":[2,3], 22 | "site_ids":[4,5], 23 | "tag_associated_asset_ids":[6,7], 24 | "search_criteria":null 25 | }, 26 | "tag_type":"CUSTOM" 27 | } -------------------------------------------------------------------------------- /test_fixtures/ReportConfigResponse_Samples.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vulnerable-exploited 7 | vulnerable-version 8 | potential 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo/sslfix.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | 5 | # SOURCE: http://stackoverflow.com/questions/11772847/error-urlopen-error-errno-8-ssl-c504-eof-occurred-in-violation-of-protoco 6 | import ssl 7 | from functools import wraps 8 | from future import standard_library 9 | standard_library.install_aliases() 10 | 11 | 12 | def sslwrap(func): 13 | @wraps(func) 14 | def bar(*args, **kw): 15 | kw['ssl_version'] = ssl.PROTOCOL_TLSv1 16 | return func(*args, **kw) 17 | return bar 18 | 19 | 20 | def patch(): 21 | if hasattr(ssl, '_create_unverified_context'): 22 | # Python >=2.7.9 23 | ssl._create_default_https_context = ssl._create_unverified_context 24 | else: 25 | # Python <2.7.9 26 | ssl.wrap_socket = sslwrap(ssl.wrap_socket) 27 | -------------------------------------------------------------------------------- /tests/test_LoadFixture.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from .load_unittest import unittest 5 | from .LoadFixture import LoadFixture 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | 9 | 10 | class LoadFixtureTestCase(unittest.TestCase): 11 | def testThatOurFixturesWillLoadCorrectly(self): 12 | fixture = LoadFixture("default_tags.json") 13 | self.assertEqual(5, fixture["total_available"]) 14 | self.assertEqual(5, len(fixture["resources"])) 15 | 16 | def testThatLoadingNonExistingFixtureResultsInAnException(self): 17 | self.assertRaises(Exception, lambda: LoadFixture("should_not_exist.json")) 18 | 19 | def testThatLoadingInvalidFixtureTypeResultsInAnException(self): 20 | self.assertRaises(ValueError, lambda: LoadFixture("xml_fixtures.py")) 21 | -------------------------------------------------------------------------------- /test_fixtures/data-scan_complete-assets_Nexpose6.json: -------------------------------------------------------------------------------- 1 | {"records":[{"operatingSystem":"Ubuntu Linux 14.04","nodeID":18,"scanStatus":"C","isMobile":false,"ipAddress":"172.16.254.138","scanStatusTranslation":"Completed","idType":"database","scanEngineName":"Local scan engine","duration":111334,"scanID":7,"assetID":"3","vulnerabilityCount":59,"id":"18","hostName":"nexpose"},{"operatingSystem":"Microsoft Windows","nodeID":17,"scanStatus":"C","isMobile":false,"ipAddress":"172.16.254.129","scanStatusTranslation":"Completed","idType":"database","scanEngineName":"Local scan engine","duration":85550,"scanID":7,"assetID":"4","vulnerabilityCount":2,"id":"17","hostName":"MAC_VM-WIN10"},{"operatingSystem":null,"nodeID":16,"scanStatus":"C","isMobile":false,"ipAddress":"172.16.254.1","scanStatusTranslation":"Completed","idType":"database","scanEngineName":"Local scan engine","duration":20923,"scanID":7,"assetID":"1","vulnerabilityCount":0,"id":"16","hostName":null}],"rowsPerPage":10,"sortColumn":"ipAddress","recordOffset":0,"sortDirection":"DESC","totalRecords":3} -------------------------------------------------------------------------------- /nexpose/nexpose_status.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from future import standard_library 6 | standard_library.install_aliases() 7 | 8 | 9 | class NexposeStatus(object): 10 | STARTING = 'starting' 11 | NORMAL_MODE = 'normal_mode' 12 | MAINTENANCE_MODE = 'maintenance_mode' 13 | UNKNOWN = 'unknown' 14 | 15 | # To be compatible with older Nexpose versions; do not remove items from the list below, only add! 16 | _URL_TO_STATUS = { 17 | 'starting.html': STARTING, 18 | 'login.html': NORMAL_MODE, 19 | 'login.jsp': NORMAL_MODE, 20 | 'maintenance-login.html': MAINTENANCE_MODE, 21 | } 22 | 23 | @staticmethod 24 | def GetStatusFromURL(url): 25 | path = url.split('/')[-1] # get the last part of the URL 26 | status = NexposeStatus._URL_TO_STATUS.get(path, NexposeStatus.UNKNOWN) 27 | return status 28 | -------------------------------------------------------------------------------- /nexpose/nexpose_backup.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from future import standard_library 6 | standard_library.install_aliases() 7 | 8 | 9 | class Backup(object): 10 | @staticmethod 11 | def CreateFromJSON(json_dict): 12 | backup = Backup() 13 | backup.name = json_dict["Download"] 14 | backup.date = json_dict["Date"] # Time ... / 1000 ? Timezone ? 15 | backup.description = json_dict["Description"] 16 | backup.nexpose_version = json_dict["Version"] 17 | backup.platform_independent = json_dict["Platform-Independent"] 18 | backup.size = int(json_dict["Size"]) 19 | return backup 20 | 21 | def __init__(self): 22 | self.name = '' 23 | self.date = '' 24 | self.description = '' 25 | self.nexpose_version = '' 26 | self.platform_independent = False 27 | self.size = 0 28 | -------------------------------------------------------------------------------- /nexpose/nexpose_userauthenticator.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from .xml_utils import get_attribute 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | 9 | 10 | class UserAuthenticatorSummary(object): 11 | @staticmethod 12 | def CreateFromXML(xml_data): 13 | summary = UserAuthenticatorSummary() 14 | summary.id = int(get_attribute(xml_data, 'id', summary.id)) 15 | summary.source = get_attribute(xml_data, 'authSource', summary.source) 16 | summary.module = get_attribute(xml_data, 'authModule', summary.module) 17 | summary.is_external = get_attribute(xml_data, 'external') == '1' 18 | return summary 19 | 20 | def __init__(self): 21 | self.id = 0 22 | self.source = '' 23 | self.module = '' 24 | self.is_external = False 25 | 26 | def AsXML(self): 27 | raise NotImplementedError(__func__) # noqa F821 28 | -------------------------------------------------------------------------------- /tests/LoadFixture.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | import json 5 | import lxml 6 | from os import path 7 | from future import standard_library 8 | standard_library.install_aliases() 9 | 10 | _SCRIPT_PATH = path.dirname(path.abspath(__file__)) 11 | _FIXTURES_PATH = path.join(_SCRIPT_PATH, "..", "test_fixtures") 12 | 13 | XML = 'xml' 14 | JSON = 'json' 15 | 16 | 17 | def LoadFixture(filename): 18 | _, _, fixture_type = filename.rpartition('.') 19 | fixture_path = path.join(_FIXTURES_PATH, filename) 20 | with open(fixture_path) as data_file: 21 | if fixture_type == JSON: 22 | return json.load(data_file) 23 | if fixture_type == XML: 24 | return lxml.etree.fromstring(data_file.read()) 25 | raise ValueError("unknown fixture type") 26 | 27 | 28 | def CreateEmptyFixture(fixture_type): 29 | if fixture_type == XML: 30 | return lxml.etree.fromstring('<_ />') 31 | raise ValueError("unknown fixture type") 32 | -------------------------------------------------------------------------------- /nexpose/python_utils.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | import inspect 5 | from future import standard_library 6 | standard_library.install_aliases() 7 | 8 | 9 | def is_subclass_of(variable, required_class): 10 | return inspect.isclass(variable) and issubclass(variable, required_class) 11 | 12 | 13 | def is_iterable(variable): 14 | return hasattr(variable, '__iter__') 15 | 16 | 17 | def remove_front_slash(uri): 18 | if uri.startswith('/'): 19 | uri = uri[1:] 20 | return uri 21 | 22 | 23 | # based on : http://stackoverflow.com/questions/6480723/urllib-urlencode-doesnt-like-unicode-values-how-about-this-workaround 24 | def utf8_encoded(data): 25 | if isinstance(data, str): 26 | return data.encode('utf8') 27 | 28 | if isinstance(data, str): 29 | # ensure now it can be decoded aka 'is valid UTF-8?' 30 | data.decode('utf8') 31 | return data 32 | 33 | if isinstance(data, dict): 34 | return dict(zip(iter(data.keys()), map(utf8_encoded, iter(data.values())))) 35 | 36 | if is_iterable(data): 37 | return list(map(utf8_encoded, data)) 38 | 39 | # not sure how to handle this data type, return as-is 40 | return data 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | 5 | def readme(): 6 | with open('README.rst', 'r') as f: 7 | return f.read() 8 | 9 | 10 | packages = [ 11 | 'nexpose', 12 | ] 13 | 14 | requires = [ 15 | 'lxml', 16 | 'future' 17 | ] 18 | 19 | setup( 20 | name='nexpose', 21 | packages=packages, 22 | package_data={'': ['LICENSE']}, 23 | package_dir={'nexpose': 'nexpose'}, 24 | include_package_data=True, 25 | version='0.1.7', 26 | license='BSD', 27 | description='The official Python Nexpose API client library', 28 | long_description=readme(), 29 | install_requires=requires, 30 | author='Davinsi Labs', 31 | url='https://github.com/rapid7/nexpose-client-python', 32 | download_url='https://github.com/rapid7/nexpose-client-python/releases', 33 | keywords=['nexpose', 'insightvm', 'rapid7'], 34 | classifiers=( 35 | 'Development Status :: 3 - Alpha', 36 | 'Intended Audience :: Developers', 37 | 'Natural Language :: English', 38 | 'Programming Language :: Python', 39 | 'Programming Language :: Python :: 2.6', 40 | 'Programming Language :: Python :: 2.7', 41 | 'Programming Language :: Python :: 3', 42 | 'Programming Language :: Python :: 3.4', 43 | 'Programming Language :: Python :: 3.5', 44 | 'Programming Language :: Python :: 3.6', 45 | ), 46 | ) 47 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | ## Screenshots (if appropriate): 16 | 17 | 18 | ## Types of changes 19 | 20 | - Bug fix (non-breaking change which fixes an issue) 21 | - New feature (non-breaking change which adds functionality) 22 | - Breaking change (fix or feature that would cause existing functionality to change) 23 | 24 | ## Checklist: 25 | 26 | 27 | - [ ] I have updated the documentation accordingly (if changes are required). 28 | - [ ] I have added tests to cover my changes. 29 | - [ ] All new and existing tests passed. 30 | 31 | -------------------------------------------------------------------------------- /nexpose/json_utils.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | 5 | from builtins import object 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | 9 | 10 | class JSON(object): 11 | @staticmethod 12 | def CreateFromJSON(json_dict): 13 | raise NotImplementedError 14 | 15 | def as_json(self): 16 | raise NotImplementedError 17 | 18 | 19 | class HasID(object): 20 | pass 21 | 22 | 23 | def get_id(data, id_field_name): 24 | if isinstance(data, HasID): 25 | return data.id 26 | if isinstance(data, dict): 27 | return data.get(id_field_name, 0) 28 | return data # assume the data is the id 29 | 30 | 31 | def load_urls(json_dict, url_loader, ignore_error=False): 32 | from urllib.error import HTTPError 33 | assert isinstance(json_dict, dict) 34 | for key in list(json_dict.keys()): 35 | if isinstance(json_dict[key], dict): 36 | if json_dict[key].get('json', None) is not None: 37 | raise ValueError('json_dict[' + key + '] already contains a json-element') 38 | url = json_dict[key].get('url', None) 39 | if url is not None: 40 | try: 41 | json_dict[key]['json'] = url_loader(url) 42 | except HTTPError: 43 | if not ignore_error: 44 | raise 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016-2018, Davinsi Labs, Rapid7, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ## Expected Behavior 6 | 7 | 8 | 9 | ## Current Behavior 10 | 11 | 12 | 13 | ## Possible Solution 14 | 15 | 16 | 17 | ## Steps to Reproduce (for bugs) 18 | 19 | 20 | 21 | 22 | Python code that reproduces the issue: 23 | ```python 24 | print("this is only an example Python code block") 25 | ``` 26 | 27 | ## Context 28 | 29 | 30 | 31 | ## Your Environment 32 | 33 | 34 | * Nexpose-client-python version: 35 | * Python version: 36 | * Operating System and version: 37 | * Nexpose product version: 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # original entries 2 | /wingide/dl_nexpose.wpu 3 | /demo/demo.cfg 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # IPython Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | # Idea files 96 | /.idea/ 97 | 98 | # Linux files 99 | *.swp 100 | 101 | # Editor Specific 102 | .vscode -------------------------------------------------------------------------------- /nexpose/nexpose_node.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from future import standard_library 6 | standard_library.install_aliases() 7 | 8 | 9 | class AssetHostTypes(object): 10 | Empty = '' 11 | Guest = 'GUEST' 12 | Hypervisor = 'HYPERVISOR' 13 | Physical = 'PHYSICAL' 14 | Mobile = 'MOBILE' 15 | 16 | 17 | class NodeScanStatus(object): 18 | UNKNOWN = '' 19 | COMPLETE = 'C' 20 | 21 | 22 | # NOTE: the tags below are available but are currently not copied to a NodeBase object 23 | # id 24 | # idType 25 | # isMobile 26 | # scanEngineName 27 | # scanStatusTranslation 28 | # vulnerabilityCount 29 | class NodeBase(object): 30 | def InitializeFromJSON(self, json_dict): 31 | self.id = json_dict['nodeID'] 32 | self.asset_id = json_dict['assetID'] 33 | self.host_name = json_dict['hostName'] 34 | self.os_name = json_dict['operatingSystem'] 35 | self.ip_address = json_dict['ipAddress'] 36 | self.scan_id = json_dict['scanID'] 37 | self.scan_status = json_dict['scanStatus'] 38 | self.scan_duration = json_dict['duration'] # TODO: is this in ms? 39 | 40 | def __init__(self): 41 | self.id = 0 42 | self.asset_id = 0 43 | self.ip_address = '' 44 | self.host_name = '' 45 | self.os_name = '' 46 | self.scan_id = 0 47 | self.scan_status = NodeScanStatus.UNKNOWN 48 | self.scan_duration = 0 49 | 50 | 51 | class Node(NodeBase): 52 | @staticmethod 53 | def CreateFromJSON(json_dict): 54 | node = Node() 55 | NodeBase.InitializeFromJSON(node, json_dict) 56 | -------------------------------------------------------------------------------- /tests/test_NexposeUserAuthenticatorSummary.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from .load_unittest import unittest 5 | from .LoadFixture import CreateEmptyFixture, LoadFixture, XML 6 | from nexpose.nexpose_userauthenticator import UserAuthenticatorSummary 7 | from future import standard_library 8 | standard_library.install_aliases() 9 | 10 | 11 | class NexposeUserAuthenticatorSummaryTestCase(unittest.TestCase): 12 | def testCreateFromXML(self): 13 | fixture = CreateEmptyFixture(XML) 14 | authenticator = UserAuthenticatorSummary.CreateFromXML(fixture) 15 | self.assertEquals(0, authenticator.id) 16 | self.assertEquals('', authenticator.source) 17 | self.assertEquals('', authenticator.module) 18 | self.assertEquals(False, authenticator.is_external) 19 | 20 | fixture = LoadFixture('UserAuthenticatorListingResponse.xml') 21 | 22 | authenticator = UserAuthenticatorSummary.CreateFromXML(fixture[0]) 23 | self.assertEquals(1, authenticator.id) 24 | self.assertEquals('Builtin Administrators', authenticator.source) 25 | self.assertEquals('XML', authenticator.module) 26 | self.assertEquals(False, authenticator.is_external) 27 | 28 | authenticator = UserAuthenticatorSummary.CreateFromXML(fixture[1]) 29 | self.assertEquals(2, authenticator.id) 30 | self.assertEquals('Builtin Users', authenticator.source) 31 | self.assertEquals('DataStore', authenticator.module) 32 | self.assertEquals(False, authenticator.is_external) 33 | 34 | authenticator = UserAuthenticatorSummary.CreateFromXML(fixture[2]) 35 | self.assertEquals(3, authenticator.id) 36 | self.assertEquals('My Users', authenticator.source) 37 | self.assertEquals('LDAP', authenticator.module) 38 | self.assertEquals(True, authenticator.is_external) 39 | -------------------------------------------------------------------------------- /tests/NexposeSessionSpy.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from nexpose.nexpose import NexposeSession, NexposeConnectionException 6 | from nexpose.xml_utils import as_xml 7 | from future import standard_library 8 | standard_library.install_aliases() 9 | 10 | 11 | class NexposeSessionSpy(NexposeSession): 12 | def __init__(self, host, port, username, password): 13 | NexposeSession.__init__(self, host, port, username, password) 14 | self.XmlStringToReturnOnExecute = None 15 | 16 | def GetURI_APIv1d1(self): 17 | return self._URI_APIv1d1 18 | 19 | def GetLoginRequest(self): 20 | return self._login_request 21 | 22 | def GetSessionID(self): 23 | return self._session_id 24 | 25 | def SetSessionID(self, session_id): 26 | self._session_id = session_id 27 | 28 | def _Execute_Fake(self, request): 29 | try: 30 | if self.XmlStringToReturnOnExecute: 31 | return as_xml(self.XmlStringToReturnOnExecute) 32 | return request # return the request as an answer 33 | except Exception as ex: 34 | raise NexposeConnectionException("Unable to execute the fake request: {0}!".format(ex), ex) 35 | 36 | def _Execute_APIv1d1(self, request): 37 | return self._Execute_Fake(request) 38 | 39 | def _Execute_APIv1d2(self, request): # TODO: write tests that use this function ? TDD ? 40 | return self._Execute_Fake(request) 41 | 42 | 43 | class SpyFactory(object): 44 | @staticmethod 45 | def CreateEmpty(): 46 | return NexposeSessionSpy(host="", port=0, username="", password="") 47 | 48 | @staticmethod 49 | def CreateWithDefaultLogin(host, port=3780): 50 | return NexposeSessionSpy(host, port, "nxadmin", "nxpassword") 51 | 52 | @staticmethod 53 | def CreateOpenSession(session_id): 54 | session = SpyFactory.CreateEmpty() 55 | session.SetSessionID(session_id) 56 | return session 57 | -------------------------------------------------------------------------------- /nexpose/nexpose_privileges.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from future import standard_library 6 | standard_library.install_aliases() 7 | 8 | 9 | class AssetGroupPrivileges(object): 10 | ConfigureAssets = 'ConfigureAssets' 11 | ConfigureAssets = 'ViewAssetData' 12 | 13 | 14 | class GlobalPrivileges(object): 15 | AddUsersToGroup = 'AddUsersToGroup' 16 | AddUsersToReport = 'AddUsersToReport' 17 | AddUsersToSite = 'AddUsersToSite' 18 | ApproveVulnExceptions = 'ApproveVulnExceptions' 19 | ApproveVulnerabilityExceptions = ApproveVulnExceptions 20 | CloseTickets = 'CloseTickets' 21 | ConfigureGlobalSettings = 'ConfigureGlobalSettings' 22 | CreateReports = 'CreateReports' 23 | CreateTickets = 'CreateTickets' 24 | DeleteVulnExceptions = 'DeleteVulnExceptions' 25 | DeleteVulnerabilityExceptions = DeleteVulnExceptions 26 | GenerateRestrictedReports = 'GenerateRestrictedReports' 27 | ManageAssetGroups = 'ManageAssetGroups' 28 | ManageDynamicAssetGroups = 'ManageDynamicAssetGroups' 29 | ManagePolicies = 'ManagePolicies' 30 | ManageReportTemplates = 'ManageReportTemplates' 31 | ManageScanEngines = 'ManageScanEngines' 32 | ManageScanTemplates = 'ManageScanTemplates' 33 | ManageSites = 'ManageSites' 34 | ManageTags = 'ManageTags' 35 | SubmitVulnExceptions = 'SubmitVulnExceptions' 36 | SubmitVulnerabilityExceptions = SubmitVulnExceptions 37 | TicketAssignee = 'TicketAssignee' 38 | 39 | 40 | class SitePrivileges(object): 41 | ConfigureAlerts = 'ConfigureAlerts' 42 | ConfigureCredentials = 'ConfigureCredentials' 43 | ConfigureEngines = 'ConfigureEngines' 44 | ConfigureScanTemplates = 'ConfigureScanTemplates' 45 | ConfigureScheduleScans = 'ConfigureScheduleScans' 46 | ConfigureSiteSettings = 'ConfigureSiteSettings' 47 | ConfigureTargets = 'ConfigureTargets' 48 | ManualScans = 'ManualScans' 49 | PurgeData = 'PurgeData' 50 | ViewAssetData = 'ViewAssetData' 51 | -------------------------------------------------------------------------------- /nexpose/xml_utils.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from lxml import etree 5 | from io import StringIO 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | 9 | 10 | def create_element(tag, optional_attributes=None): 11 | request = etree.Element(tag) 12 | if optional_attributes: 13 | for tag, value in iter(optional_attributes.items()): 14 | request.attrib[tag] = "{0}".format(value) 15 | return request 16 | 17 | 18 | def get_attribute(xml_data, attribute_name, default_value=None): 19 | if xml_data is None: 20 | return default_value 21 | return xml_data.attrib.get(attribute_name, default_value) 22 | 23 | 24 | def get_children_of(xml_data, element_name): 25 | element = get_element(xml_data, element_name, default_value=None) 26 | return element.getchildren() if element is not None else () 27 | 28 | 29 | def get_element(xml_data, element_name, default_value=None): 30 | if xml_data is None: 31 | return default_value 32 | return xml_data.find(element_name) 33 | 34 | 35 | def get_content_of(xml_data, element_name, default_value=None): 36 | if xml_data is None: 37 | return default_value 38 | element = xml_data.find(element_name) 39 | if element is None: 40 | return default_value 41 | if element.text is None: 42 | return default_value 43 | return element.text 44 | 45 | 46 | def as_string(xml_data): 47 | return etree.tostring(xml_data) 48 | 49 | 50 | def from_large_string(s): 51 | return etree.XML(s.encode('utf-8')) 52 | 53 | 54 | def as_xml(s): 55 | # Note: 56 | # There is a bug in the StartUpdateResponse, in case of a failure (no internet connection), 57 | # two StartUpdateResponse XML objects are returned, one indicating failure, one indicating success. 58 | # We handle this bug here (wrong place?!), by embedding the returned XML in a single object 59 | # and returning the first element after conversion. 60 | if s.startswith('' + s + '' 63 | return from_large_string(s).getchildren()[0] 64 | -------------------------------------------------------------------------------- /nexpose/nexpose_assetgroup.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from .xml_utils import get_attribute, get_element, as_string 6 | from .nexpose_asset import AssetSummary 7 | from future import standard_library 8 | standard_library.install_aliases() 9 | 10 | 11 | class _AssetGroupBase(object): 12 | def InitializeFromXML(self, xml_data): 13 | self.id = int(get_attribute(xml_data, 'id', self.id)) 14 | self.name = get_attribute(xml_data, 'name', self.name) 15 | self.short_description = get_attribute(xml_data, 'description', self.short_description) 16 | 17 | def __init__(self): 18 | self.id = 0 19 | self.name = 0 20 | self.short_description = '' # API 1.1 removes newlines that were added through the UI 21 | 22 | 23 | class AssetGroupSummary(_AssetGroupBase): 24 | @staticmethod 25 | def CreateFromXML(xml_data): 26 | asset_group = AssetGroupSummary() 27 | asset_group.InitializeFromXML(xml_data) 28 | asset_group.risk_score = float(get_attribute(xml_data, 'riskscore', asset_group.risk_score)) 29 | return asset_group 30 | 31 | def __init__(self): 32 | _AssetGroupBase.__init__(self) 33 | self.risk_score = 0.0 34 | 35 | 36 | class AssetGroupConfiguration(_AssetGroupBase): 37 | @staticmethod 38 | def CreateFromXML(xml_data): 39 | xml_devices = get_element(xml_data, 'Devices', None) 40 | print(as_string(xml_data)) 41 | config = AssetGroupConfiguration() 42 | config.InitializeFromXML(xml_data) 43 | config.description = config.short_description 44 | if xml_devices is not None: 45 | config.asset_summaries = [AssetSummary.CreateFromXML(xml_device) for xml_device in xml_devices.getchildren() if xml_device.tag == 'device'] 46 | return config 47 | 48 | @staticmethod 49 | def Create(): 50 | config = AssetGroupConfiguration() 51 | config.id = -1 52 | return config 53 | 54 | def __init__(self): 55 | _AssetGroupBase.__init__(self) 56 | self.description = None 57 | self.asset_summaries = [] 58 | -------------------------------------------------------------------------------- /nexpose/nexpose_criteria_operators.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from future import standard_library 6 | from future.utils import with_metaclass 7 | 8 | standard_library.install_aliases() 9 | 10 | 11 | class MetaNexposeCriteriaOperator(type): 12 | @property 13 | def Code(cls): 14 | return cls.__name__ 15 | 16 | def __str__(cls): 17 | return cls.Code 18 | 19 | 20 | class NexposeCriteriaOperator(with_metaclass(MetaNexposeCriteriaOperator, object)): 21 | pass 22 | 23 | 24 | class AND(NexposeCriteriaOperator): 25 | pass 26 | 27 | 28 | class OR(NexposeCriteriaOperator): 29 | pass 30 | 31 | 32 | class ARE(NexposeCriteriaOperator): 33 | pass 34 | 35 | 36 | class IS(NexposeCriteriaOperator): 37 | pass 38 | 39 | 40 | class IS_NOT(NexposeCriteriaOperator): 41 | pass 42 | 43 | 44 | class STARTS_WITH(NexposeCriteriaOperator): 45 | pass 46 | 47 | 48 | class ENDS_WITH(NexposeCriteriaOperator): 49 | pass 50 | 51 | 52 | class IS_EMPTY(NexposeCriteriaOperator): 53 | pass 54 | 55 | 56 | class IS_NOT_EMPTY(NexposeCriteriaOperator): 57 | pass 58 | 59 | 60 | class IS_APPLIED(NexposeCriteriaOperator): 61 | pass 62 | 63 | 64 | class IS_NOT_APPLIED(NexposeCriteriaOperator): 65 | pass 66 | 67 | 68 | class CONTAINS(NexposeCriteriaOperator): 69 | pass 70 | 71 | 72 | class NOT_CONTAINS(NexposeCriteriaOperator): 73 | pass 74 | 75 | 76 | class INCLUDE(NexposeCriteriaOperator): 77 | pass 78 | 79 | 80 | class DO_NOT_INCLUDE(NexposeCriteriaOperator): 81 | pass 82 | 83 | 84 | class IN(NexposeCriteriaOperator): 85 | pass 86 | 87 | 88 | class NOT_IN(NexposeCriteriaOperator): 89 | pass 90 | 91 | 92 | class IN_RANGE(NexposeCriteriaOperator): 93 | pass 94 | 95 | 96 | class LESS_THAN(NexposeCriteriaOperator): 97 | pass 98 | 99 | 100 | class GREATER_THAN(NexposeCriteriaOperator): 101 | pass 102 | 103 | 104 | class ON_OR_BEFORE(NexposeCriteriaOperator): 105 | pass 106 | 107 | 108 | class ON_OR_AFTER(NexposeCriteriaOperator): 109 | pass 110 | 111 | 112 | class BETWEEN(NexposeCriteriaOperator): 113 | pass 114 | 115 | 116 | class EARLIER_THAN(NexposeCriteriaOperator): 117 | pass 118 | 119 | 120 | class WITHIN_THE_LAST(NexposeCriteriaOperator): 121 | pass 122 | -------------------------------------------------------------------------------- /nexpose/nexpose_assetfilter.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from .json_utils import JSON 5 | from .nexpose_criteria import Criteria, Criterion 6 | from .nexpose_asset import AssetBase 7 | import json 8 | from future import standard_library 9 | standard_library.install_aliases() 10 | 11 | 12 | class FilteredAsset(AssetBase, JSON): 13 | @staticmethod 14 | def CreateFromJSON(json_dict): 15 | asset = FilteredAsset() 16 | asset.id = json_dict['assetID'] 17 | asset.risk_score = json_dict['riskScore'] 18 | 19 | asset.assessed = json_dict['assessed'] 20 | asset.malware_count = json_dict['malwareCount'] 21 | asset.vulnerability_count = json_dict['vulnCount'] 22 | asset.exploit_count = json_dict['exploitCount'] 23 | asset.asset_name = json_dict['assetName'] # TODO: could be 'host' from AssetSummary ? 24 | asset.os_id = json_dict['assetOSID'] 25 | 26 | # see also AssetSummary 27 | asset.site_id = json_dict['sitePermissions'][0]['siteID'] 28 | 29 | # see also AssetDetails 30 | asset.os_name = json_dict['assetOSName'] 31 | asset.ip_address = json_dict['assetIP'] 32 | asset.lastScanDate = json_dict['lastScanDate'] # TODO: convert to Date object ? 33 | 34 | # Replace JSON-nulls by empty strings 35 | if asset.os_name is None: 36 | asset.os_name = '' 37 | if asset.asset_name is None: 38 | asset.asset_name = '' 39 | 40 | return asset 41 | 42 | def __init__(self): 43 | AssetBase.__init__(self) 44 | 45 | self.assessed = False 46 | self.malware_count = 0 47 | self.vulnerability_count = 0 48 | self.exploit_count = 0 49 | self.asset_name = '' # TODO: could be 'host' from AssetSummary ? 50 | self.os_id = 0 51 | 52 | # see also AssetSummary 53 | self.site_id = 0 54 | 55 | # see also AssetDetails 56 | self.os_name = None 57 | self.ip_address = '' 58 | self.last_scan_date = '' 59 | 60 | 61 | class AssetFilter(JSON): 62 | def __init__(self, criteria_or_criterion): 63 | if isinstance(criteria_or_criterion, Criterion): 64 | criteria_or_criterion = Criteria.Create(criteria_or_criterion) 65 | assert isinstance(criteria_or_criterion, Criteria) 66 | self.criteria = criteria_or_criterion 67 | 68 | def as_json(self): 69 | js = dict() 70 | js['dir'] = 'ASC' 71 | js['sort'] = 'assetIP' # why can't we sort on ID? 72 | js['table-id'] = 'assetfilter' 73 | js['searchCriteria'] = json.dumps(self.criteria.as_json(), separators=(',', ':')) 74 | return js 75 | -------------------------------------------------------------------------------- /nexpose/nexpose_enginepool.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from .xml_utils import get_attribute, create_element 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | 9 | 10 | class EnginePoolBase(object): 11 | def InitalizeFromXML(self, xml_data): 12 | self.id = int(get_attribute(xml_data, 'id', self.id)) 13 | self.name = get_attribute(xml_data, 'name', self.name) 14 | self.host = get_attribute(xml_data, 'address', self.host) 15 | self.port = int(get_attribute(xml_data, 'port', self.port)) 16 | self.scope = get_attribute(xml_data, 'scope', self.scope) 17 | 18 | def __init__(self): 19 | self.id = 0 20 | self.name = '' 21 | self.host = '' 22 | self.port = 40814 23 | self.scope = 'silo' 24 | 25 | 26 | class EnginePoolSummary(EnginePoolBase): 27 | @staticmethod 28 | def CreateFromXML(xml_data): 29 | summary = EnginePoolSummary() 30 | summary.InitalizeFromXML(xml_data) 31 | summary.status = get_attribute(xml_data, 'status', summary.status) 32 | return summary 33 | 34 | def __init__(self): 35 | EnginePoolBase.__init__(self) 36 | self.status = EnginePoolStatus.Unknown # noqa F821 37 | 38 | 39 | class EnginePoolConfiguration(EnginePoolBase): 40 | @staticmethod 41 | def CreateFromXML(xml_data): 42 | config = EnginePoolConfiguration() 43 | config.InitalizeFromXML(xml_data) 44 | config.priority = get_attribute(xml_data, 'priority', config.priority) 45 | config.assigned_sites = [(int(get_attribute(xml_site, 'id', 0)), get_attribute(xml_data, 'name', '')) for xml_site in xml_data.getchildren() if xml_site.tag == 'Site'] 46 | return config 47 | 48 | @staticmethod 49 | def Create(): 50 | config = EnginePoolConfiguration() 51 | config.id = -1 52 | return config 53 | 54 | @staticmethod 55 | def CreateNamed(name): 56 | config = EnginePoolConfiguration.Create() 57 | config.name = name 58 | return config 59 | 60 | def __init__(self): 61 | EnginePoolBase.__init__(self) 62 | self.priority = EnginePoolPriority.Normal # noqa F821 63 | self.assigned_sites = [] 64 | 65 | def AsXML(self, exclude_id): 66 | attributes = {} 67 | if not exclude_id: 68 | attributes['id'] = self.id 69 | attributes['name'] = self.name 70 | attributes['address'] = self.host 71 | attributes['port'] = self.port 72 | attributes['scope'] = self.scope 73 | attributes['priority'] = self.priority 74 | xml_data = create_element('EnginePoolConfig', attributes) 75 | return xml_data 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | The [RESTful API for the Nexpose/InsightVM Security Console](https://help.rapid7.com/insightvm/en-us/api/index.html) has rendered this library obsolete. If you require a Python library for that API you can use a [generated client](https://github.com/rapid7/vm-console-client-python). Clients for other languages can be generated from the Swagger specification. Note that generated clients are not officially supported or maintained by Rapid7. 3 | 4 | This project will not receive new changes from Rapid7, though pull requests may still be accepted and new releases published on request. 5 | 6 | # nexpose-client-python 7 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) [![Travis](https://img.shields.io/travis/rapid7/nexpose-client-python.svg)](https://travis-ci.org/rapid7/nexpose-client-python) [![PyPI Version](https://img.shields.io/pypi/v/nexpose.svg)](https://pypi.python.org/pypi/nexpose) ![PyPI Status](https://img.shields.io/pypi/status/nexpose.svg) [![GitHub license](https://img.shields.io/badge/license-BSD-blue.svg)](https://raw.githubusercontent.com/rapid7/nexpose-client-python/master/LICENSE) ![PyPI Pythons](https://img.shields.io/pypi/pyversions/nexpose.svg) 8 | 9 | This is the official Python package for the Python Nexpose API client library. 10 | 11 | For assistance with using the library or to discuss different approaches, please open an issue. To share or discuss scripts which use the library head over to the [Nexpose Resources](https://github.com/rapid7/nexpose-resources) project. 12 | 13 | Check out the [wiki](https://github.com/rapid7/nexpose-client-python/wiki) for walk-throughs and other documentation. Submit bugs and feature requests on the [issues](https://github.com/rapid7/nexpose-client-python/issues) page. 14 | 15 | This library provides calls to the Nexpose XML APIs version 1.1 and 1.2. 16 | 17 | nexpose-client-python uses [Semantic Versioning](http://semver.org/). This allows for confident use of [version pinning](https://www.python.org/dev/peps/pep-0440/#version-specifiers) in your requirements file. 18 | 19 | Install the library using pip: `pip install nexpose` 20 | 21 | ## Release Notes 22 | 23 | Release notes are available on the [Releases](https://github.com/rapid7/nexpose-client-python/releases) page. 24 | 25 | ## Contributions 26 | 27 | We welcome contributions to this package. Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 28 | 29 | Full usage examples or task-oriented scripts should be submitted to the [Nexpose Resources](https://github.com/rapid7/nexpose-resources) project. Smaller examples can be added to the [wiki](https://github.com/rapid7/nexpose-client-python/wiki). 30 | 31 | ## License 32 | 33 | The nexpose-client-python library is provided under the 3-Clause BSD License. See [LICENSE](./LICENSE) for details. 34 | 35 | ## Credits 36 | Davinsi Labs 37 | Rapid7, Inc. 38 | 39 | See [contributors](./contributors.md) for more info. 40 | -------------------------------------------------------------------------------- /test_fixtures/default_tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "page":1, 3 | "sort":null, 4 | "resources":[ 5 | { 6 | "attributes":[ 7 | { 8 | "tag_attribute_name":"SOURCE", 9 | "tag_attribute_value":"Built-in", 10 | "tag_attribute_id":2 11 | }, 12 | { 13 | "tag_attribute_name":"RISK_MODIFIER", 14 | "tag_attribute_value":"2.0", 15 | "tag_attribute_id":1 16 | } 17 | ], 18 | "url":"https://localhost/api/2.0/tags/1", 19 | "asset_ids":[ 20 | 21 | ], 22 | "tag_config":null, 23 | "tag_id":1, 24 | "tag_type":"CRITICALITY", 25 | "tag_name":"Very High" 26 | }, 27 | { 28 | "attributes":[ 29 | { 30 | "tag_attribute_name":"RISK_MODIFIER", 31 | "tag_attribute_value":"1.5", 32 | "tag_attribute_id":5 33 | }, 34 | { 35 | "tag_attribute_name":"SOURCE", 36 | "tag_attribute_value":"Built-in", 37 | "tag_attribute_id":6 38 | } 39 | ], 40 | "url":"https://localhost/api/2.0/tags/2", 41 | "asset_ids":[ 42 | 43 | ], 44 | "tag_config":null, 45 | "tag_id":2, 46 | "tag_type":"CRITICALITY", 47 | "tag_name":"High" 48 | }, 49 | { 50 | "attributes":[ 51 | { 52 | "tag_attribute_name":"RISK_MODIFIER", 53 | "tag_attribute_value":"1.0", 54 | "tag_attribute_id":9 55 | }, 56 | { 57 | "tag_attribute_name":"SOURCE", 58 | "tag_attribute_value":"Built-in", 59 | "tag_attribute_id":10 60 | } 61 | ], 62 | "url":"https://localhost/api/2.0/tags/3", 63 | "asset_ids":[ 64 | 65 | ], 66 | "tag_config":null, 67 | "tag_id":3, 68 | "tag_type":"CRITICALITY", 69 | "tag_name":"Medium" 70 | }, 71 | { 72 | "attributes":[ 73 | { 74 | "tag_attribute_name":"SOURCE", 75 | "tag_attribute_value":"Built-in", 76 | "tag_attribute_id":14 77 | }, 78 | { 79 | "tag_attribute_name":"RISK_MODIFIER", 80 | "tag_attribute_value":"0.75", 81 | "tag_attribute_id":13 82 | } 83 | ], 84 | "url":"https://localhost/api/2.0/tags/4", 85 | "asset_ids":[ 86 | 87 | ], 88 | "tag_config":null, 89 | "tag_id":4, 90 | "tag_type":"CRITICALITY", 91 | "tag_name":"Low" 92 | }, 93 | { 94 | "attributes":[ 95 | { 96 | "tag_attribute_name":"RISK_MODIFIER", 97 | "tag_attribute_value":"0.5", 98 | "tag_attribute_id":17 99 | }, 100 | { 101 | "tag_attribute_name":"SOURCE", 102 | "tag_attribute_value":"Built-in", 103 | "tag_attribute_id":18 104 | } 105 | ], 106 | "url":"https://localhost/api/2.0/tags/5", 107 | "asset_ids":[ 108 | 109 | ], 110 | "tag_config":null, 111 | "tag_id":5, 112 | "tag_type":"CRITICALITY", 113 | "tag_name":"Very Low" 114 | } 115 | ], 116 | "url":"https://localhost/api/2.0/tags?per_page=2147483647", 117 | "per_page":2147483647, 118 | "total_available":5, 119 | "first_url":"https://localhost/api/2.0/tags?per_page=2147483647\u0026page=1", 120 | "last_url":"https://localhost/api/2.0/tags?per_page=2147483647\u0026page=1", 121 | "next_url":null, 122 | "previous_url":null 123 | } -------------------------------------------------------------------------------- /nexpose/nexpose_engine.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from .xml_utils import get_attribute, create_element 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | 9 | 10 | class EnginePriority(object): 11 | VeryLow = 'very-low' 12 | Low = 'low' 13 | Normal = 'normal' 14 | High = 'high' 15 | VeryHigh = 'very high' 16 | 17 | 18 | class EngineStatus(object): 19 | Active = 'active' 20 | PendingAuthorization = 'pending-authorization' 21 | Incompatible = 'incompatible' 22 | NotResponding = 'not-responding' 23 | Unknown = 'unknown' 24 | 25 | 26 | class EngineBase(object): 27 | def InitalizeFromXML(self, xml_data): 28 | self.id = int(get_attribute(xml_data, 'id', self.id)) 29 | self.name = get_attribute(xml_data, 'name', self.name) 30 | self.host = get_attribute(xml_data, 'address', self.host) 31 | self.port = int(get_attribute(xml_data, 'port', self.port)) 32 | self.scope = get_attribute(xml_data, 'scope', self.scope) 33 | 34 | def __init__(self): 35 | self.id = 0 36 | self.name = '' 37 | self.host = '' 38 | self.port = 40814 39 | self.scope = 'silo' 40 | 41 | 42 | class EngineSummary(EngineBase): 43 | @staticmethod 44 | def CreateFromXML(xml_data): 45 | summary = EngineSummary() 46 | summary.InitalizeFromXML(xml_data) 47 | summary.status = get_attribute(xml_data, 'status', summary.status) 48 | return summary 49 | 50 | def __init__(self): 51 | EngineBase.__init__(self) 52 | self.status = EngineStatus.Unknown 53 | 54 | 55 | class EngineConfiguration(EngineBase): 56 | @staticmethod 57 | def CreateFromXML(xml_data): 58 | config = EngineConfiguration() 59 | config.InitalizeFromXML(xml_data) 60 | config.priority = get_attribute(xml_data, 'priority', config.priority) 61 | config.assigned_sites = [(int(get_attribute(xml_site, 'id', 0)), get_attribute(xml_site, 'name', '')) for xml_site in xml_data.getchildren() if xml_site.tag == 'Site'] 62 | return config 63 | 64 | @staticmethod 65 | def Create(): 66 | config = EngineConfiguration() 67 | config.id = -1 68 | return config 69 | 70 | @staticmethod 71 | def CreateNamed(name): 72 | config = EngineConfiguration.Create() 73 | config.name = name 74 | return config 75 | 76 | def __init__(self): 77 | EngineBase.__init__(self) 78 | self.priority = EnginePriority.Normal 79 | self.assigned_sites = [] 80 | 81 | def AsXML(self, exclude_id): 82 | attributes = {} 83 | if not exclude_id: 84 | attributes['id'] = self.id 85 | attributes['name'] = self.name 86 | attributes['address'] = self.host 87 | attributes['port'] = self.port 88 | attributes['scope'] = self.scope 89 | attributes['priority'] = self.priority 90 | xml_data = create_element('EngineConfig', attributes) 91 | return xml_data 92 | -------------------------------------------------------------------------------- /nexpose/nexpose_vulnerability.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from .xml_utils import get_attribute, get_element, as_string 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | 9 | 10 | class VulnerabilityReference(object): 11 | @staticmethod 12 | def CreateFromXML(xml_data): 13 | reference = VulnerabilityReference() 14 | reference.source = get_attribute(xml_data, 'source', reference.source) 15 | return reference 16 | 17 | def __init__(self): 18 | self.source = '' 19 | 20 | 21 | class VulnerabilityBase(object): 22 | def InitalizeFromXML(self, xml_data): 23 | self.id = get_attribute(xml_data, 'id', self.id) 24 | self.title = get_attribute(xml_data, 'title', self.title) 25 | self.severity = int(get_attribute(xml_data, 'severity', self.severity)) 26 | self.pci_severity = int(get_attribute(xml_data, 'pciSeverity', self.pci_severity)) 27 | self.cvss_score = float(get_attribute(xml_data, 'cvssScore', self.cvss_score)) 28 | self.cvss_vector = get_attribute(xml_data, 'cvssVector', self.cvss_vector) 29 | self.requires_credentials = get_attribute(xml_data, 'requiresCredentials', self.requires_credentials) in ['1', 'true'] 30 | self.is_safe = get_attribute(xml_data, 'safe', self.is_safe) in ['1', 'true'] 31 | self.published = get_attribute(xml_data, 'published', self.published) 32 | self.added = get_attribute(xml_data, 'added', self.added) 33 | self.modified = get_attribute(xml_data, 'modified', self.modified) 34 | 35 | def __init__(self): 36 | self.id = '' 37 | self.title = '' 38 | self.severity = 0 39 | self.pci_severity = 0 40 | self.cvss_score = 0 41 | self.cvss_vector = '' 42 | self.requires_credentials = False 43 | self.is_safe = False 44 | self.published = '' 45 | self.added = '' 46 | self.modified = '' 47 | 48 | 49 | class VulnerabilitySummary(VulnerabilityBase): 50 | @staticmethod 51 | def CreateFromXML(xml_data): 52 | summary = VulnerabilitySummary() 53 | summary.InitalizeFromXML(xml_data) 54 | return summary 55 | 56 | def __init__(self): 57 | VulnerabilityBase.__init__(self) 58 | 59 | 60 | class VulnerabilityDetail(VulnerabilityBase): 61 | @staticmethod 62 | def CreateFromXML(xml_data): 63 | xml_description = get_element(xml_data, 'description') 64 | xml_solution = get_element(xml_data, 'solution') 65 | 66 | reference_generator = map(lambda xml_reference: VulnerabilityReference.CreateFromXML(xml_reference), xml_data.iterfind('references/reference')) 67 | config = VulnerabilityDetail() 68 | config.InitalizeFromXML(xml_data) 69 | config.description = as_string(xml_description) if xml_description is not None else config.description 70 | config.references = list(reference_generator) 71 | config.solution = as_string(xml_solution) if xml_solution is not None else config.solution 72 | return config 73 | 74 | def __init__(self): 75 | VulnerabilityBase.__init__(self) 76 | self.description = '' 77 | self.references = [] 78 | self.solution = '' 79 | -------------------------------------------------------------------------------- /nexpose/nexpose_report.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from .xml_utils import get_attribute 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | 9 | 10 | class ReportStatus(object): 11 | STARTED = 'Started' 12 | GENERATED = 'Generated' 13 | FAILED = 'Failed' 14 | ABORTED = 'Aborted' 15 | UNKNOWN = 'Unknown' 16 | 17 | 18 | class ReportTemplate(object): 19 | pass 20 | 21 | 22 | # TODO: test the difference between global and silo scoped reports 23 | # and refactor accordingly 24 | class _ReportBase(object): 25 | def _InitalizeFromXML(self, xml_data, name_of_id_field): 26 | self.id = int(get_attribute(xml_data, name_of_id_field, self.id)) 27 | self.status = get_attribute(xml_data, 'status', self.status) 28 | self.generated_on = get_attribute(xml_data, 'generated-on', self.generated_on) # TODO: parse this as a date 29 | self.URI = get_attribute(xml_data, 'report-URI', self.URI) 30 | self.scope = get_attribute(xml_data, 'scope', self.scope) 31 | 32 | def __init__(self): 33 | self.id = 0 34 | self.status = ReportStatus.UNKNOWN 35 | self.generated_on = '' # TODO: default date? 36 | self.URI = '' 37 | self.scope = 'silo' 38 | 39 | 40 | # TODO: test the difference between global and silo scoped reports 41 | # and refactor accordingly 42 | class _ReportConfigurationBase(object): 43 | def _InitalizeFromXML(self, xml_data): 44 | self.template_id = get_attribute(xml_data, 'template-id', self.template_id) 45 | self.name = get_attribute(xml_data, 'name', self.name) 46 | 47 | def __init__(self): 48 | self.template_id = '' 49 | self.name = '' 50 | 51 | 52 | class ReportSummary(_ReportBase): 53 | @staticmethod 54 | def CreateFromXML(xml_data): 55 | summary = ReportSummary() 56 | _ReportBase._InitalizeFromXML(summary, xml_data, 'id') 57 | summary.configuration_id = int(get_attribute(xml_data, 'cfg-id', summary.configuration_id)) 58 | return summary 59 | 60 | def __init__(self): 61 | _ReportBase.__init__(self) 62 | self.configuration_id = 0 63 | 64 | 65 | class ReportConfigurationSummary(_ReportBase, _ReportConfigurationBase): 66 | @staticmethod 67 | def CreateFromXML(xml_data): 68 | config = ReportConfigurationSummary() 69 | _ReportBase._InitalizeFromXML(config, xml_data, 'cfg-id') 70 | _ReportConfigurationBase._InitalizeFromXML(config, xml_data) 71 | return config 72 | 73 | def __init__(self): 74 | _ReportBase.__init__(self) 75 | _ReportConfigurationBase.__init__(self) 76 | 77 | 78 | class ReportConfiguration(_ReportConfigurationBase): 79 | @staticmethod 80 | def CreateFromXML(xml_data): 81 | config = ReportConfiguration() 82 | _ReportConfigurationBase._InitalizeFromXML(config, xml_data) 83 | return config 84 | 85 | def __init__(self): 86 | _ReportConfigurationBase.__init__(self) 87 | self.format = '' 88 | self.owner = '' 89 | self.timezone = '' 90 | self.description = '' 91 | self.filters = [] 92 | self.baseline = '' # TODO: default date? 93 | self.users = [] 94 | self.generate = None 95 | self.delivery = '' 96 | self.dbexport = '' 97 | self.credentials = '' 98 | self.parameter_name = '' # TODO: ?? 99 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | DEPRECATED 2 | ========== 3 | 4 | The `RESTful API for the Nexpose/InsightVM Security 5 | Console `__ has 6 | rendered this library obsolete. If you require a Python library for that 7 | API you can use a `generated 8 | client `__. Clients 9 | for other languages can be generated from the Swagger specification. 10 | Note that generated clients are not officially supported or maintained 11 | by Rapid7. 12 | 13 | This project will not receive new changes from Rapid7, though pull 14 | requests may still be accepted and new releases published on request. 15 | 16 | nexpose-client-python 17 | ===================== 18 | 19 | |No Maintenance Intended| |Travis| |PyPI Version| |PyPI Status| |GitHub 20 | license| |PyPI Pythons| 21 | 22 | This is the official Python package for the Python Nexpose API client 23 | library. 24 | 25 | For assistance with using the library or to discuss different 26 | approaches, please open an issue. To share or discuss scripts which use 27 | the library head over to the `Nexpose 28 | Resources `__ project. 29 | 30 | Check out the 31 | `wiki `__ for 32 | walk-throughs and other documentation. Submit bugs and feature requests 33 | on the 34 | `issues `__ 35 | page. 36 | 37 | This library provides calls to the Nexpose XML APIs version 1.1 and 1.2. 38 | 39 | nexpose-client-python uses `Semantic Versioning `__. 40 | This allows for confident use of `version 41 | pinning `__ 42 | in your requirements file. 43 | 44 | Install the library using pip: ``pip install nexpose`` 45 | 46 | Release Notes 47 | ------------- 48 | 49 | Release notes are available on the 50 | `Releases `__ 51 | page. 52 | 53 | Contributions 54 | ------------- 55 | 56 | We welcome contributions to this package. Please see 57 | `CONTRIBUTING `__ for details. 58 | 59 | Full usage examples or task-oriented scripts should be submitted to the 60 | `Nexpose Resources `__ 61 | project. Smaller examples can be added to the 62 | `wiki `__. 63 | 64 | License 65 | ------- 66 | 67 | The nexpose-client-python library is provided under the 3-Clause BSD 68 | License. See `LICENSE `__ for details. 69 | 70 | Credits 71 | ------- 72 | 73 | | Davinsi Labs 74 | | Rapid7, Inc. 75 | 76 | See `contributors `__ for more info. 77 | 78 | .. |No Maintenance Intended| image:: http://unmaintained.tech/badge.svg 79 | :target: http://unmaintained.tech/ 80 | .. |Travis| image:: https://img.shields.io/travis/rapid7/nexpose-client-python.svg 81 | :target: https://travis-ci.org/rapid7/nexpose-client-python 82 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/nexpose.svg 83 | :target: https://pypi.python.org/pypi/nexpose 84 | .. |PyPI Status| image:: https://img.shields.io/pypi/status/nexpose.svg 85 | .. |GitHub license| image:: https://img.shields.io/badge/license-BSD-blue.svg 86 | :target: https://raw.githubusercontent.com/rapid7/nexpose-client-python/master/LICENSE 87 | .. |PyPI Pythons| image:: https://img.shields.io/pypi/pyversions/nexpose.svg 88 | -------------------------------------------------------------------------------- /nexpose/nexpose_discoveryconnection.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, unicode_literals) 3 | from builtins import object 4 | from future import standard_library 5 | standard_library.install_aliases() 6 | from .xml_utils import get_attribute, create_element 7 | from urllib.parse import urlparse 8 | 9 | 10 | class DiscoveryConnectionProtocol(object): 11 | HTTP = 'http' 12 | HTTPS = 'https' 13 | 14 | 15 | class _DiscoveryConnectionBase(object): 16 | def InitalizeFromXML(self, xml_data): 17 | self.id = int(get_attribute(xml_data, 'id', self.id)) 18 | self.name = get_attribute(xml_data, 'name', self.name) 19 | self.host = get_attribute(xml_data, 'address', self.host) 20 | self.port = int(get_attribute(xml_data, 'port', self.port)) 21 | self.protocol = get_attribute(xml_data, 'protocol', self.protocol).lower() 22 | self.username = get_attribute(xml_data, 'user-name', self.username) 23 | self.password = get_attribute(xml_data, 'password', self.password) 24 | # TODO: according to the manual a : is added, I doubt that, untested yet 25 | if self.protocol.endswith(':'): 26 | self.protocol = self.protocol[:-1] 27 | 28 | def __init__(self): 29 | self.id = 0 30 | self.name = '' 31 | self.host = '' 32 | self.port = 0 33 | self.protocol = '' 34 | self.username = '' 35 | self.password = '' 36 | 37 | 38 | class DiscoveryConnectionSummary(_DiscoveryConnectionBase): 39 | @staticmethod 40 | def CreateFromXML(xml_data): 41 | summary = DiscoveryConnectionSummary() 42 | summary.InitalizeFromXML(xml_data) 43 | return summary 44 | 45 | def __init__(self): 46 | _DiscoveryConnectionBase.__init__(self) 47 | 48 | 49 | class DiscoveryConnectionConfiguration(DiscoveryConnectionSummary): 50 | @staticmethod 51 | def CreateFromXML(xml_data): 52 | config = DiscoveryConnectionConfiguration() 53 | config.InitalizeFromXML(xml_data) 54 | return config 55 | 56 | @staticmethod 57 | def Create(): 58 | config = DiscoveryConnectionConfiguration() 59 | config.id = -1 60 | return config 61 | 62 | @staticmethod 63 | def CreateNamed(name): 64 | config = DiscoveryConnectionConfiguration.Create() 65 | config.name = name 66 | return config 67 | 68 | @staticmethod 69 | def CreateNamedFromURL(name, url, username=None, password=None): 70 | parsed_url = urlparse(url) 71 | host, _, port = parsed_url.netloc.rpartition(':') 72 | if host == '': 73 | host = port 74 | port = '80' if parsed_url.scheme == 'http' else '443' 75 | config = DiscoveryConnectionConfiguration.CreateNamed(name) 76 | config.protocol = parsed_url.scheme.upper() 77 | config.host = host 78 | config.port = port 79 | config.username = '' if username is None else username 80 | config.password = '' if password is None else password 81 | return config 82 | 83 | def __init__(self): 84 | _DiscoveryConnectionBase.__init__(self) 85 | 86 | def AsXML(self, exclude_id): 87 | attributes = {} 88 | if not exclude_id: 89 | attributes['id'] = self.id 90 | attributes['name'] = self.name 91 | attributes['address'] = self.host 92 | attributes['port'] = self.port 93 | attributes['protocol'] = self.protocol 94 | attributes['user-name'] = self.username 95 | attributes['password'] = self.password 96 | xml_data = create_element('DiscoveryConnection', attributes) 97 | return xml_data 98 | -------------------------------------------------------------------------------- /nexpose/nexpose_vulnerabilityexception.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from builtins import object 5 | from .xml_utils import get_attribute, get_content_of 6 | from future import standard_library 7 | standard_library.install_aliases() 8 | 9 | 10 | def fix_null(data): 11 | if data == 'null': 12 | return 0 13 | return data 14 | 15 | 16 | class VulnerabilityExceptionStatus(object): 17 | UNDER_REVIEW = "Under Review" 18 | APPROVED = "Approved" 19 | REJECTED = "Rejected" 20 | DELETED = "Deleted" # This state is also used for recalled exceptions! 21 | 22 | 23 | class VulnerabilityExceptionReason(object): 24 | FALSE_POSITIVE = "False Positive" 25 | COMPENSATING_CONTROL = "Compensating Control" 26 | ACCEPTABLE_USE = "Acceptable Use" 27 | ACCEPTABLE_RISK = "Acceptable Risk" 28 | OTHER = "Other" 29 | 30 | 31 | class VulnerabilityExceptionScope(object): 32 | ALL_INSTANCES = "All Instances" 33 | ALL_INSTANCES_SPECIFIC_ASSET = "All Instances on a Specific Asset" 34 | ALL_INSTANCES_SPECIFIC_SITE = "All Instances on a Specific Site" 35 | SPECIFIC_INSTANCE_SPECIFIC_ASSET = "Specific Instance of Specific Asset" 36 | 37 | 38 | class SiloVulnerabilityExceptionDetails(object): 39 | @staticmethod 40 | def CreateFromXML(xml_data): 41 | details = SiloVulnerabilityExceptionDetails() 42 | details.silo_id = get_attribute(xml_data, 'siloId', details.silo_id) 43 | details.oldest_exception_creation_date = get_attribute(xml_data, 'oldestExceptionCreationDate', details.oldest_exception_creation_date) # TODO: date object 44 | details.pending_exception_count = get_attribute(xml_data, 'pendingVulnExceptionsCount', details.pending_exception_count) 45 | return details 46 | 47 | def __init__(self): 48 | self.silo_id = '' 49 | self.oldest_exception_creation_date = 'N/A' # TODO: date object 50 | self.pending_exception_count = 0 51 | 52 | 53 | class VulnerabilityException(object): 54 | @staticmethod 55 | def CreateFromXML(xml_data): 56 | details = VulnerabilityException() 57 | details.id = int(get_attribute(xml_data, 'exception-id', details.id)) 58 | details.vulnerability_id = get_attribute(xml_data, 'vuln-id', details.vulnerability_id) 59 | details.vulnerability_key = get_attribute(xml_data, 'vuln-key', details.vulnerability_key) 60 | details.expiration_date = get_attribute(xml_data, 'expiration-date', details.expiration_date) # TODO: date object 61 | details.submitter = get_attribute(xml_data, 'submitter', details.submitter) 62 | details.submitter_comment = get_content_of(xml_data, 'submitter-comment', details.submitter_comment) 63 | details.reviewer = get_attribute(xml_data, 'reviewer', details.reviewer) 64 | details.reviewer_comment = get_content_of(xml_data, 'reviewer-comment', details.reviewer_comment) 65 | details.status = get_attribute(xml_data, 'status', details.status) 66 | details.reason = get_attribute(xml_data, 'reason', details.reason) 67 | details.scope = get_attribute(xml_data, 'scope', details.scope) 68 | details.asset_id = int(fix_null(get_attribute(xml_data, 'device-id', details.asset_id))) 69 | details.asset_port = int(fix_null(get_attribute(xml_data, 'port-no', details.asset_port))) 70 | return details 71 | 72 | def __init__(self): 73 | self.id = 0 74 | self.vulnerability_id = '' 75 | self.vulnerability_key = '' 76 | self.expiration_date = '' # TODO: date object 77 | self.submitter = '' 78 | self.submitter_comment = '' 79 | self.reviewer = '' 80 | self.reviewer_comment = '' 81 | self.status = '' 82 | self.reason = '' 83 | self.scope = '' 84 | self.asset_id = 0 85 | self.asset_port = 0 86 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to nexpose-client-python 2 | 3 | The users and maintainers of nexpose-client-python would greatly appreciate any contributions 4 | you can make to the project. These contributions typically come in the form of 5 | filed bugs/issues or pull requests (PRs). These contributions routinely result 6 | in new versions of the [nexpose-client-python library](https://pypi.python.org/pypi/nexpose) and the 7 | [nexpose-client-python release](https://github.com/rapid7/nexpose-client-python/releases) to be released. The 8 | process for each is outlined below. 9 | 10 | ## Contributing Issues / Bug Reports 11 | 12 | If you encounter any bugs or problems with nexpose-client-python, please file them 13 | [here](https://github.com/rapid7/nexpose-client-python/issues/new), providing as much detail as 14 | possible. If the bug is straight-forward enough and you understand the fix for 15 | the bug well enough, you may take the simpler, less-paperwork route and simply 16 | file a PR with the fix and the necessary details. 17 | 18 | ## Contributing Code 19 | 20 | nexpose-client-python uses a model nearly identical to that of 21 | [Metasploit](https://github.com/rapid7/metasploit-framework) as outlined 22 | [here](https://github.com/rapid7/metasploit-framework/wiki/Setting-Up-a-Metasploit-Development-Environment), 23 | at least from a ```git``` perspective. If you've been through that process 24 | (or, even better, you've been through it many times with many people), you can 25 | do exactly what you did for Metasploit but with nexpose-client-python and ignore the rest of 26 | this document. 27 | 28 | On the other hand, if you haven't, read on! 29 | 30 | ### Fork and Clone 31 | 32 | Generally, this should only need to be done once, or if you need to start over. 33 | 34 | 1. Fork nexpose-client: Visit https://github.com/rapid7/nexpose-client-python and click Fork, 35 | selecting your github account if prompted 36 | 2. Clone ```git@github.com:/nexpose-client-python.git```, replacing 37 | `````` with, you guessed it, your Github username. 38 | 3. Add the master nexpose-client-python repository as your upstream: 39 | ``` 40 | git remote add upstream git://github.com/rapid7/nexpose-client-python.git 41 | git fetch --all 42 | ``` 43 | 44 | ### Branch and Improve 45 | 46 | If you have a contribution to make, first create a branch to contain your 47 | work. The name is yours to choose, however generally it should roughly 48 | describe what you are doing. In this example, and from here on out, the 49 | branch will be wow, but you should change this. 50 | 51 | ``` 52 | git fetch --all 53 | git checkout master 54 | git rebase upstream/master 55 | git checkout -b wow 56 | ``` 57 | 58 | Now, make your changes, committing as necessary, using useful commit messages: 59 | 60 | ``` 61 | vim CONTRIBUTING.md 62 | git add CONTRIBUTING.md 63 | git commit -m "Adds a document on how to contribute to nexpose-client-python." -a 64 | ``` 65 | 66 | Please note that changes to [version.py](version.py) in PRs are almost never necessary. 67 | 68 | Now push your changes to your fork: 69 | 70 | ``` 71 | git push origin wow 72 | ``` 73 | 74 | Finally, submit the PR. Navigate to ```https://github.com//nexpose-client-python/compare/wow```, fill in the details, and submit. 75 | 76 | ## Releasing New Versions 77 | 78 | Typically this process is reserved for contributors with push permissions to 79 | nexpose-client-python: 80 | 81 | Be sure to regenerate the README.rst file if the README.md has changed. Use `pandoc -s -r markdown -w rst README.md -o README.rst` and validate the link URLs. 82 | 83 | ### Pypi Release 84 | 85 | Pypi releases, for use with the `pip` command, are performed by a Jenkins job. Currently Jenkins access is restricted to Rapid7 employees. The package will be published at [https://pypi.python.org/pypi/nexpose](https://pypi.python.org/pypi/nexpose). 86 | 87 | ### Github Release 88 | 89 | Some users may prefer to consume nexpose-client-python in a manner other than using git itself. For that reason, Github offers [Releases](https://github.com/blog/1547-release-your-software). Whenever a new version of the software is to be released, be kind and also create a new [Release](https://github.com/rapid7/nexpose-client-python/releases), using a versioning scheme identical to that used for the library. 90 | -------------------------------------------------------------------------------- /tests/test_NexposeReportConfigurationSummary.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from .load_unittest import unittest 5 | from .LoadFixture import CreateEmptyFixture, LoadFixture, XML, JSON 6 | from nexpose.nexpose_report import ReportStatus, ReportConfigurationSummary, ReportSummary 7 | from future import standard_library 8 | standard_library.install_aliases() 9 | 10 | 11 | class NexposeReportConfigurationSummaryTestCase(unittest.TestCase): 12 | def testCreateFromXML(self): 13 | fixture = CreateEmptyFixture(XML) 14 | report_cfg = ReportConfigurationSummary.CreateFromXML(fixture) 15 | self.assertEquals(0, report_cfg.id) 16 | self.assertEquals('', report_cfg.template_id) 17 | self.assertEquals('', report_cfg.name) 18 | self.assertEquals(ReportStatus.UNKNOWN, report_cfg.status) 19 | self.assertEquals('', report_cfg.generated_on) 20 | self.assertEquals('', report_cfg.URI) 21 | self.assertEquals('silo', report_cfg.scope) 22 | 23 | fixture = LoadFixture('ReportListingResponse.xml') 24 | 25 | report_cfg = ReportConfigurationSummary.CreateFromXML(fixture[0]) 26 | self.assertEquals(2, report_cfg.id) 27 | self.assertEquals('audit-report', report_cfg.template_id) 28 | self.assertEquals('Report 2.0 - Complete Site', report_cfg.name) 29 | self.assertEquals(ReportStatus.GENERATED, report_cfg.status) 30 | self.assertEquals('20160303T122452808', report_cfg.generated_on) 31 | self.assertEquals('/reports/00000002/00000002/report.xml', report_cfg.URI) 32 | self.assertEquals('silo', report_cfg.scope) 33 | 34 | report_cfg = ReportConfigurationSummary.CreateFromXML(fixture[1]) 35 | self.assertEquals(3, report_cfg.id) 36 | self.assertEquals('audit-report', report_cfg.template_id) 37 | self.assertEquals('Report 2.0 - Including non-vuln', report_cfg.name) 38 | self.assertEquals(ReportStatus.GENERATED, report_cfg.status) 39 | self.assertEquals('20160303T122922250', report_cfg.generated_on) 40 | self.assertEquals('/reports/00000003/00000003/report.xml', report_cfg.URI) 41 | self.assertEquals('silo', report_cfg.scope) 42 | 43 | report_cfg = ReportConfigurationSummary.CreateFromXML(fixture[2]) 44 | self.assertEquals(1, report_cfg.id) 45 | self.assertEquals('audit-report', report_cfg.template_id) 46 | self.assertEquals('XML 2.0 export', report_cfg.name) 47 | self.assertEquals(ReportStatus.GENERATED, report_cfg.status) 48 | self.assertEquals('20160219T062407874', report_cfg.generated_on) 49 | self.assertEquals('/reports/00000001/00000001/report.xml', report_cfg.URI) 50 | self.assertEquals('global', report_cfg.scope) 51 | 52 | 53 | class NexposeReportSummaryTestCase(unittest.TestCase): 54 | def testCreateFromXML(self): 55 | fixture = CreateEmptyFixture(XML) 56 | report_summary = ReportSummary.CreateFromXML(fixture) 57 | self.assertEquals(0, report_summary.id) 58 | self.assertEquals(ReportStatus.UNKNOWN, report_summary.status) 59 | self.assertEquals('', report_summary.generated_on) 60 | self.assertEquals('', report_summary.URI) 61 | self.assertEquals('silo', report_summary.scope) 62 | 63 | fixture = LoadFixture('ReportHistoryResponse.xml') 64 | 65 | report_summary = ReportSummary.CreateFromXML(fixture[0]) 66 | self.assertEquals(6, report_summary.id) 67 | self.assertEquals(2, report_summary.configuration_id) 68 | self.assertEquals(ReportStatus.GENERATED, report_summary.status) 69 | self.assertEquals('20160303T161938459', report_summary.generated_on) 70 | self.assertEquals('/reports/00000002/00000006/report.xml', report_summary.URI) 71 | self.assertEquals('silo', report_summary.scope) 72 | 73 | report_summary = ReportSummary.CreateFromXML(fixture[1]) 74 | self.assertEquals(2, report_summary.id) 75 | self.assertEquals(2, report_summary.configuration_id) 76 | self.assertEquals(ReportStatus.GENERATED, report_summary.status) 77 | self.assertEquals('20160303T122452808', report_summary.generated_on) 78 | self.assertEquals('/reports/00000002/00000002/report.xml', report_summary.URI) 79 | self.assertEquals('silo', report_summary.scope) 80 | -------------------------------------------------------------------------------- /tests/test_NexposeSession.py: -------------------------------------------------------------------------------- 1 | # Future Imports for py2/3 backwards compat. 2 | from __future__ import (absolute_import, division, print_function, 3 | unicode_literals) 4 | from .load_unittest import unittest 5 | from nexpose.nexpose import NexposeFailureException, NexposeException, SessionIsNotClosedException 6 | from nexpose.xml_utils import as_string, as_xml 7 | from .NexposeSessionSpy import NexposeSessionSpy, SpyFactory 8 | from future import standard_library 9 | standard_library.install_aliases() 10 | 11 | FAKE_SESSIONID = "B33R" 12 | FAKE_SITEID = 201 13 | 14 | # hackish 15 | SpyFactory_CreateOpenSession = SpyFactory.CreateOpenSession 16 | SpyFactory.CreateOpenSession = staticmethod(lambda: SpyFactory_CreateOpenSession(FAKE_SESSIONID)) 17 | 18 | 19 | class NexposeSessionTestCase(unittest.TestCase): 20 | def assertEqualXml(self, xml_object, xml_string): 21 | self.assertEqual(as_string(xml_object), as_string(as_xml(xml_string))) 22 | 23 | def testDefaultConstructionOfURI_APIv1d1(self): 24 | expected_uri = "https://localhost:3780/api/1.1/xml" 25 | session = SpyFactory.CreateEmpty() 26 | self.assertEqual(session.GetURI_APIv1d1(), expected_uri) 27 | 28 | def testConstructionOfURI_APIv1d1(self): 29 | expected_uri = "https://nexpose.davansilabs.local:666/api/1.1/xml" 30 | session = SpyFactory.CreateWithDefaultLogin("nexpose.davansilabs.local", 666) 31 | self.assertEqual(session.GetURI_APIv1d1(), expected_uri) 32 | 33 | def testConstructionOfLoginRequest(self): 34 | expected_request = [b'', b''] 35 | session = SpyFactory.CreateWithDefaultLogin("server") 36 | self.assertIn(as_string(session.GetLoginRequest()), expected_request) 37 | 38 | def testCorrectLogin(self): 39 | session = SpyFactory.CreateWithDefaultLogin('*') 40 | session.XmlStringToReturnOnExecute = ''.format(FAKE_SESSIONID) 41 | session.Open() 42 | self.assertEqual(session.GetSessionID(), FAKE_SESSIONID) 43 | 44 | def testIncorrectLogin(self): 45 | session = SpyFactory.CreateWithDefaultLogin('*') 46 | session.XmlStringToReturnOnExecute = ''.format(FAKE_SESSIONID) 47 | self.assertEqual(session.GetSessionID(), None) 48 | self.assertRaises(NexposeFailureException, session.Open) 49 | self.assertNotEqual(session.GetSessionID(), FAKE_SESSIONID) 50 | 51 | def testLoginWithInvalidHtmlReply(self): 52 | session = SpyFactory.CreateWithDefaultLogin('*') 53 | session.XmlStringToReturnOnExecute = '