From 82edc83a69e39d1a53f726d6513778141d013219 Mon Sep 17 00:00:00 2001 From: Gavin Schneider Date: Tue, 3 Oct 2017 20:08:55 -0700 Subject: [PATCH 1/9] WIP: Add support for report configs --- nexpose/nexpose_report.py | 327 +++++++++++++++++++++++++++++++++++--- 1 file changed, 302 insertions(+), 25 deletions(-) diff --git a/nexpose/nexpose_report.py b/nexpose/nexpose_report.py index 08a0fab..5281391 100644 --- a/nexpose/nexpose_report.py +++ b/nexpose/nexpose_report.py @@ -2,7 +2,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) from builtins import object -from .xml_utils import get_attribute +from .xml_utils import get_attribute, get_content_of, get_children_of, create_element, as_string, as_xml, get_element from future import standard_library standard_library.install_aliases() @@ -19,8 +19,6 @@ class ReportTemplate(object): pass -# TODO: test the difference between global and silo scoped reports -# and refactor accordingly class _ReportBase(object): def _InitalizeFromXML(self, xml_data, name_of_id_field): self.id = int(get_attribute(xml_data, name_of_id_field, self.id)) @@ -37,16 +35,14 @@ def __init__(self): self.scope = 'silo' -# TODO: test the difference between global and silo scoped reports -# and refactor accordingly class _ReportConfigurationBase(object): def _InitalizeFromXML(self, xml_data): self.template_id = get_attribute(xml_data, 'template-id', self.template_id) self.name = get_attribute(xml_data, 'name', self.name) - def __init__(self): - self.template_id = '' - self.name = '' + def __init__(self, template_id=None, name=None): + self.template_id = template_id + self.name = name class ReportSummary(_ReportBase): @@ -75,24 +71,305 @@ def __init__(self): _ReportConfigurationBase.__init__(self) -class ReportConfiguration(_ReportConfigurationBase): +class AdhocReportConfiguration(_ReportConfigurationBase): + def __init__(self, template_id, format, site_id=None, owner=None, timezone=None): + _ReportConfigurationBase.__init__(self, template_id) + self.format = format + self.owner = owner + self.timezone = timezone + self.language = None + self.filters = [] + self.baseline = None + + if site_id: + self.add_filter('site', site_id) + + def add_filter(self, type, id): + self.filters.append(Filter(type, id)) + + def add_common_vuln_filters(self): + for vuln_filter in ['vulnerable-exploited', 'vulnerable-version', 'potential']: + self.add_filter('vuln-status', vuln_filter) + + def as_xml(self): + attributes = {'name': self.name, 'template-id': self.template_id, 'format': self.format} + if self.owner: + attributes['owner'] = self.owner + if self.timezone: + attributes['timezone'] = self.timezone + + xml_data = create_element('AdhocReportConfig', attributes) + + xml_filters = create_element('Filters') + for report_filter in self.filters: + xml_filters.append(report_filter.as_xml()) + xml_data.append(xml_filters) + + if self.baseline: + xml_baseline = create_element('Baseline', {'compareTo': self.baseline}) + else: + xml_baseline = create_element('Baseline') + xml_data.append(xml_baseline) + + return xml_data + + +class ReportConfiguration(AdhocReportConfiguration): @staticmethod - def CreateFromXML(xml_data): - config = ReportConfiguration() - _ReportConfigurationBase._InitalizeFromXML(config, xml_data) - return config + def create_from_xml(xml_data): + template_id = get_attribute(xml_data, 'template-id') + name = get_attribute(xml_data, 'name') + format = get_attribute(xml_data, 'format') - def __init__(self): - _ReportConfigurationBase.__init__(self) - self.format = '' - self.owner = '' - self.timezone = '' - self.description = '' - self.filters = [] - self.baseline = '' # TODO: default date? + return ReportConfiguration(name, template_id, format) + + def _initialze_from_xml(self, xml_data): + self.template_id = get_attribute(xml_data, 'template-id', self.template_id) + self.name = get_attribute(xml_data, 'name', self.name) + + def __init__(self, name, template_id, format, id=-1, owner=None, timezone=None): + """ + + :param name: The name of the report configuration + :type name: str + :param template_id: The report template ID + :type template_id: str + :param format: The report output format (pdf|html|rtf|xml|text|csv|db|raw-xml|raw-xml-v2|ns-xml|qualys-xml|sql) + :type format: str + :param id: The report configuration ID, or -1 for a new configuration + :type id: int + :param owner: The user ID of the report owner + :type owner: int or None + :param timezone: The timezone of the report by name, e.g. America/Los_Angeles for Pacific (PDT/PST) + :type timezone: str + """ + AdhocReportConfiguration.__init__(self, template_id, format, None, owner, timezone) + self.name = name + self.id = id self.users = [] self.generate = None - self.delivery = '' - self.dbexport = '' - self.credentials = '' - self.parameter_name = '' # TODO: ?? + self.frequency = None + self.delivery = None + self.dbexport = None # TODO: needs DBExport class implemented + self.credentials = None # TODO: needs ExportCredential class implemented + + def as_xml(self): + attributes = {'id': self.id, 'name': self.name, 'template-id': self.template_id, 'format': self.format} + if self.owner: + attributes['owner'] = self.owner + if self.timezone: + attributes['timezone'] = self.timezone + + xml_data = create_element('ReportConfig', attributes) + + xml_filters = create_element('Filters') + for report_filter in self.filters: + xml_filters.append(report_filter.as_xml()) + + xml_data.append(xml_filters) + + if self.baseline: + xml_baseline = create_element('Baseline', {'compareTo': self.baseline}) + else: + xml_baseline = create_element('Baseline') + + xml_data.append(xml_baseline) + + xml_users = create_element('Users') + for user in self.users: + xml_users.append(create_element('user', {'id': user})) + xml_data.append(xml_users) + + if self.frequency: + xml_data.append(self.frequency.as_xml()) + + if self.delivery: + xml_data.append(self.delivery.as_xml()) + + if self.dbexport: + pass # TODO needs DBExport class implemented + + return xml_data + + +class Filter: + def __init__(self, type, id): + """ + The type can be one of: + + - site + - group + - device + - tag + - scan + - vuln-categories + - vuln-severity + - vuln-status + - cyberscope-component + - cyberscope-bureau + - cyberscope-enclave + + For site, group, device, tag, and scan the ID is the numeric ID. + For scan, the ID can also be "last" for the most recently run scan. + For vuln-status, the ID can have one of the following values: + + 1. vulnerable-exploited (The check was positive. An exploit verified the vulnerability.) + 2. vulnerable-version (The check was positive. The version of the scanned service or software is associated with + known vulnerabilities.) + 3. potential (The check for a potential vulnerability was positive.) + + These values are supported for CSV and XML formats. + + :param type: The type of filter. + :type type: str + :param id: The numeric ID or string value for the given filter. + :type id: int or str + """ + self.type = type + self.id = id + + def as_xml(self): + return create_element('filter', {self.type: self.id}) + + +class Schedule: + def __init__(self, type, interval, start): + """ + + :param type: Valid schedule types: daily, hourly, monthly-date, monthly-day, weekly. + :type type: str + :param interval: The repeat interval based upon type. + :type interval: int + :param start: Starting time of the scheduled scan (in ISO 8601 format) (yyyyMMdd'T'HHmmssSSS). + :type start: str + """ + self.type = type + self.interval = interval + self.start = start # TODO make sure this value is formatted correctly to: yyyyMMdd'T'HHmmssSSS + + def as_xml(self): + attributes = {'type': self.type, 'interval': self.interval, 'start': self.start} + return create_element('Schedule', attributes) + + +class Frequency: + def __init__(self, after_scan=False, scheduled=False, schedule=None): + """ + + :param after_scan: Whether or not to generate after scan completes on any in-scope assets, sites, groups, or tags + :type after_scan: bool + :param scheduled: Whether or not to generate this report on a schedule (cannot be used with after_scan) + :type scheduled: bool + :param schedule: The schedule for recurring report generation + :type schedule: Schedule + """ + self.after_scan = after_scan + self.scheduled = scheduled + self.schedule = schedule + + def as_xml(self): + attributes = {'after-scan': 1 if self.after_scan else 0, 'schedule': 1 if self.schedule else 0} + xml_data = create_element('Generate', attributes) + if self.schedule: + xml_data.append(self.schedule.as_xml()) + return xml_data + + +class Email: + def __init__(self, to_all_authorized, send_to_owner_as=None, send_to_acl_as=None, send_as=None): + """ + + :param to_all_authorized: Send to all the authorized users of sites, groups, and assets. + :type to_all_authorized: bool + :param send_to_owner_as: Format to send to users on the report access list (file|zip|url). + :type send_to_owner_as: str + :param send_to_acl_as: Format to send to users on the report access list (file|zip|url). + :type send_to_acl_as: str + :param send_as: Send as file attachment or zipped file to individuals who are not members (file|zip). + :type send_as: str + """ + self.to_all_authorized = to_all_authorized + self.send_to_owner_as = send_to_owner_as + self.send_to_acl_as = send_to_acl_as + self.send_as = send_as + self.sender = None + self.smtp_relay_server = None + self.recipients = [] + + def as_xml(self): + attributes = {'toAllAuthorized': 1 if self.to_all_authorized else 0} + if self.send_to_owner_as: + attributes['sendToOwnerAs'] = self.send_to_owner_as + if self.send_to_acl_as: + attributes['sendToAclAs'] = self.send_to_acl_as + if self.send_as: + attributes['sendAs'] = self.send_as + + xml_data = create_element('Email', attributes) + + if self.sender: + xml_sender = create_element('Sender') + xml_sender.text = self.sender + xml_data.append(xml_sender) + + if self.smtp_relay_server: + xml_smtp = create_element('SmtpRelayServer') + xml_smtp.text = self.smtp_relay_server + xml_data.append(xml_smtp) + + if len(self.recipients) > 0: + xml_recipients = create_element('Recipients') + for recipient in self.recipients: + xml_recipient = create_element('Recipient') + xml_recipient.text = recipient + xml_recipients.append(xml_recipient) + xml_data.append(xml_recipients) + + return xml_data + + +class Delivery: + def __init__(self, store_on_server, location=None, email=None): + """ + + :param store_on_server: Whether to store the generated report on server. + :type store_on_server: bool + :param location: Directory location to store report in (for non-default storage). + :type location: str + :param email: E-mail configuration. + :type email: Email + """ + self.store_on_server = store_on_server + self.location = location + self.email = email + + def as_xml(self): + xml_data = create_element('Delivery') + xml_storage = create_element('Storage', {'storeOnServer': 1 if self.store_on_server else 0}) + if self.location: + xml_location = create_element('location') + xml_location.text = self.location + xml_storage.append(xml_location) + + if self.email: + xml_data.append(self.email.as_xml()) + + return xml_data + + +# TODO: implement db export for report config +class DBExport: + def __init__(self): + pass + + def as_xml(self): + pass + + +# TODO: implement db export credential +class ExportCredential: + def __init__(self): + pass + + def as_xml(self): + pass From beeb49dcd64fee4af4848ab85238a96b6ec17746 Mon Sep 17 00:00:00 2001 From: Gavin Schneider Date: Wed, 4 Oct 2017 16:28:00 -0700 Subject: [PATCH 2/9] WIP: Implement saving report config, add demo code --- demo/run_demo.py | 22 +++++++++++++++++++++- nexpose/nexpose.py | 20 +++++++++++++++++--- nexpose/nexpose_report.py | 25 ++++++++++++++++++++----- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/demo/run_demo.py b/demo/run_demo.py index 5c7598d..cba760e 100755 --- a/demo/run_demo.py +++ b/demo/run_demo.py @@ -11,6 +11,7 @@ from zipfile import ZipFile import sslfix import nexpose.nexpose as nexpose +from nexpose.xml_utils import as_string, as_xml from future import standard_library standard_library.install_aliases() @@ -677,6 +678,24 @@ def DemonstrateTicketAPI(): print(" Comment:", repr(event.comment)) +def DemonstrateReportAPI(): + print('Report API') + print('----------') + report = nexpose.ReportConfiguration('Python API Client Test Report', 'audit-report', 'raw-xml-v2') + report.add_common_vuln_filters() + print('Saving report configuration...') + print(as_string(report.AsXML())) + resp = session.SaveReportConfiguration(report) + print('Saved Report ID: {}'.format(resp)) + + print('Loading report configuration with ID {}...'.format(resp)) + loaded_report = session.GetReportConfigurationDetails(resp) + print(as_string(loaded_report.AsXML())) + + print('Deleting report configuration with ID {}...'.format(resp)) + session.DeleteReportConfiguration(resp) + print('Done with Report API demo.') + def GetNexposeLoginSettings(): """ Returns a list with following information: hostname_or_ip, port, username, password. @@ -720,13 +739,14 @@ def main(): #DemonstrateVulnerabilityAPI() #DemonstrateVulnerabilityExceptionAPI() #DemonstrateRoleAPI() - DemonstrateSiteAPI() + #DemonstrateSiteAPI() #DemonstrateEngineAPI() #DemonstrateDiscoveryConnectionAPI() #DemonstrateScanPI() #DemonstrateUserAPI() #DemonstrateAssetGroupAPI() #DemonstrateTicketAPI() + DemonstrateReportAPI() #print session.GenerateScanReport(1) diff --git a/nexpose/nexpose.py b/nexpose/nexpose.py index 6de22b2..55dfcb7 100644 --- a/nexpose/nexpose.py +++ b/nexpose/nexpose.py @@ -598,8 +598,12 @@ def RequestReportConfig(self, reportconfiguration_id): """ return self.ExecuteBasicOnReportConfiguration("ReportConfigRequest", reportconfiguration_id) - def RequestReportSave(self): - raise NotImplementedError() # TODO + def RequestReportSave(self, report_configuration, generate_now=False): + """ + Save a report configuration and optionally generate a report immediately. + This function will return a single ReportSaveResponse XML object (API 1.1). + """ + return self.ExecuteBasicWithElement('ReportSaveRequest', {'generate-now': 1 if generate_now else 0}, report_configuration) def RequestReportGenerate(self, reportconfiguration_id): """ @@ -1969,7 +1973,7 @@ def GetReportConfigurationDetails(self, report_or_configuration_or_id): report_or_configuration_or_id = report_or_configuration_or_id.configuration_id response = self.VerifySuccess(self.RequestReportConfig(report_or_configuration_or_id)) element = get_element(response, 'ReportConfig') - return ReportConfigurationSummary.CreateFromXML(element) # TODO: THIS MUST BE A FULL CONFIGURATION + return ReportConfiguration.CreateFromXML(element) # TODO: THIS MUST BE A FULL CONFIGURATION def GetReportHistory(self, reportconfiguration_or_id): """ @@ -2048,6 +2052,16 @@ def GenerateScanReport(self, scan_or_id): body = ''.join(data[4:-1]) return as_xml(base64.urlsafe_b64decode(body)) + def SaveReportConfiguration(self, report_configuration, generate_now=False): + """ + Save the configuration of a report and return the id of the saved report config. + If successful, the id will also have been updated in the provided ReportConfiguration object. + To create a new report, specify -1 as id. + """ + # TODO: how to pass generate_now param downstream? + self._RequireInstanceOf(report_configuration, ReportConfiguration) + return self._ExecuteSave(self.RequestReportSave, report_configuration, 'ReportSaveResponse', 'reportcfg-id') + # # The following functions implement the Role Management API: # ========================================================= diff --git a/nexpose/nexpose_report.py b/nexpose/nexpose_report.py index 5281391..59ec1d8 100644 --- a/nexpose/nexpose_report.py +++ b/nexpose/nexpose_report.py @@ -91,7 +91,7 @@ def add_common_vuln_filters(self): for vuln_filter in ['vulnerable-exploited', 'vulnerable-version', 'potential']: self.add_filter('vuln-status', vuln_filter) - def as_xml(self): + def AsXML(self): attributes = {'name': self.name, 'template-id': self.template_id, 'format': self.format} if self.owner: attributes['owner'] = self.owner @@ -116,12 +116,21 @@ def as_xml(self): class ReportConfiguration(AdhocReportConfiguration): @staticmethod - def create_from_xml(xml_data): + def CreateFromXML(xml_data): template_id = get_attribute(xml_data, 'template-id') name = get_attribute(xml_data, 'name') format = get_attribute(xml_data, 'format') + id = get_attribute(xml_data, 'id') + owner = get_attribute(xml_data, 'owner') + timezone = get_attribute(xml_data, 'timezone') + + cfg = ReportConfiguration(name, template_id, format, id, owner, timezone) + filters = [Filter.CreateFromXML(filter) for filter in get_children_of(xml_data, 'Filters')] + cfg.filters = filters + + # TODO: draw the rest of the owl - return ReportConfiguration(name, template_id, format) + return cfg def _initialze_from_xml(self, xml_data): self.template_id = get_attribute(xml_data, 'template-id', self.template_id) @@ -153,7 +162,7 @@ def __init__(self, name, template_id, format, id=-1, owner=None, timezone=None): self.dbexport = None # TODO: needs DBExport class implemented self.credentials = None # TODO: needs ExportCredential class implemented - def as_xml(self): + def AsXML(self, exclude_id=False): attributes = {'id': self.id, 'name': self.name, 'template-id': self.template_id, 'format': self.format} if self.owner: attributes['owner'] = self.owner @@ -229,7 +238,13 @@ def __init__(self, type, id): self.id = id def as_xml(self): - return create_element('filter', {self.type: self.id}) + return create_element('filter', {'type': self.type, 'id': self.id}) + + @staticmethod + def CreateFromXML(xml_data): + type = get_attribute(xml_data, 'type') + id = get_attribute(xml_data, 'id') + return Filter(type, id) class Schedule: From 2e79a37638c109d65849ea9a7055c4d9491fe655 Mon Sep 17 00:00:00 2001 From: Gavin Schneider Date: Tue, 10 Oct 2017 12:50:58 -0700 Subject: [PATCH 3/9] Implement more of the report config, fix test and names --- demo/run_demo.py | 28 +++-- nexpose/nexpose_report.py | 100 +++++++++++++----- .../test_NexposeReportConfigurationSummary.py | 4 +- 3 files changed, 98 insertions(+), 34 deletions(-) diff --git a/demo/run_demo.py b/demo/run_demo.py index cba760e..d212acb 100755 --- a/demo/run_demo.py +++ b/demo/run_demo.py @@ -11,7 +11,6 @@ from zipfile import ZipFile import sslfix import nexpose.nexpose as nexpose -from nexpose.xml_utils import as_string, as_xml from future import standard_library standard_library.install_aliases() @@ -681,21 +680,36 @@ def DemonstrateTicketAPI(): def DemonstrateReportAPI(): print('Report API') print('----------') + + # TODO: show off more of the report API functionality than just a single config save/delete + report = nexpose.ReportConfiguration('Python API Client Test Report', 'audit-report', 'raw-xml-v2') - report.add_common_vuln_filters() + report.add_filter('scan', 'last') # this should use site/group/tag filter(s) instead of scan in real world use + report.add_common_vuln_filters() # adds vuln filters to match UI defaults + + # these imports should be at the top, or perhaps be auto imports in the init file? + from nexpose.nexpose_report import Email, Delivery, Frequency, Schedule + email = Email(True, send_as='file') + email.smtp_relay_server = 'whatever.example.com' + email.sender = 'whatever@example.com' + email.recipients.append('someone@example.com') + delivery = Delivery(True, None, email) + report.delivery = delivery + schedule = Schedule('weekly', 1, "20171105T164239700") + freq = Frequency(False, True, schedule) + report.frequency = freq + report.owner = 1 + report.timezone = 'America/Los_Angeles' + print('Saving report configuration...') - print(as_string(report.AsXML())) resp = session.SaveReportConfiguration(report) print('Saved Report ID: {}'.format(resp)) - print('Loading report configuration with ID {}...'.format(resp)) - loaded_report = session.GetReportConfigurationDetails(resp) - print(as_string(loaded_report.AsXML())) - print('Deleting report configuration with ID {}...'.format(resp)) session.DeleteReportConfiguration(resp) print('Done with Report API demo.') + def GetNexposeLoginSettings(): """ Returns a list with following information: hostname_or_ip, port, username, password. diff --git a/nexpose/nexpose_report.py b/nexpose/nexpose_report.py index 59ec1d8..f64a41e 100644 --- a/nexpose/nexpose_report.py +++ b/nexpose/nexpose_report.py @@ -1,8 +1,7 @@ # Future Imports for py2/3 backwards compat. -from __future__ import (absolute_import, division, print_function, - unicode_literals) +from __future__ import (absolute_import, division, print_function, unicode_literals) from builtins import object -from .xml_utils import get_attribute, get_content_of, get_children_of, create_element, as_string, as_xml, get_element +from .xml_utils import create_element, get_attribute, get_content_of, get_children_of, get_element from future import standard_library standard_library.install_aliases() @@ -88,7 +87,7 @@ def add_filter(self, type, id): self.filters.append(Filter(type, id)) def add_common_vuln_filters(self): - for vuln_filter in ['vulnerable-exploited', 'vulnerable-version', 'potential']: + for vuln_filter in ['potential', 'vulnerable-version', 'vulnerable-exploited']: self.add_filter('vuln-status', vuln_filter) def AsXML(self): @@ -102,7 +101,7 @@ def AsXML(self): xml_filters = create_element('Filters') for report_filter in self.filters: - xml_filters.append(report_filter.as_xml()) + xml_filters.append(report_filter.AsXML()) xml_data.append(xml_filters) if self.baseline: @@ -126,16 +125,26 @@ def CreateFromXML(xml_data): cfg = ReportConfiguration(name, template_id, format, id, owner, timezone) filters = [Filter.CreateFromXML(filter) for filter in get_children_of(xml_data, 'Filters')] - cfg.filters = filters + cfg.filters = filters if filters is not None else [] + + baseline = get_element(xml_data, 'Baseline') + cfg.baseline = get_attribute(baseline, 'compareTo') + + users = [get_attribute(user, 'id') for user in get_children_of(xml_data, 'Users')] + cfg.users = users if users is not None else [] + + frequency = get_element(xml_data, 'Generate') + if frequency is not None: + cfg.frequency = Frequency.CreateFromXML(frequency) + + delivery = get_element(xml_data, 'Delivery') + if delivery is not None: + cfg.delivery = Delivery.CreateFromXML(delivery) # TODO: draw the rest of the owl return cfg - def _initialze_from_xml(self, xml_data): - self.template_id = get_attribute(xml_data, 'template-id', self.template_id) - self.name = get_attribute(xml_data, 'name', self.name) - def __init__(self, name, template_id, format, id=-1, owner=None, timezone=None): """ @@ -173,7 +182,7 @@ def AsXML(self, exclude_id=False): xml_filters = create_element('Filters') for report_filter in self.filters: - xml_filters.append(report_filter.as_xml()) + xml_filters.append(report_filter.AsXML()) xml_data.append(xml_filters) @@ -190,10 +199,10 @@ def AsXML(self, exclude_id=False): xml_data.append(xml_users) if self.frequency: - xml_data.append(self.frequency.as_xml()) + xml_data.append(self.frequency.AsXML()) if self.delivery: - xml_data.append(self.delivery.as_xml()) + xml_data.append(self.delivery.AsXML()) if self.dbexport: pass # TODO needs DBExport class implemented @@ -237,7 +246,7 @@ def __init__(self, type, id): self.type = type self.id = id - def as_xml(self): + def AsXML(self): return create_element('filter', {'type': self.type, 'id': self.id}) @staticmethod @@ -262,18 +271,25 @@ def __init__(self, type, interval, start): self.interval = interval self.start = start # TODO make sure this value is formatted correctly to: yyyyMMdd'T'HHmmssSSS - def as_xml(self): + def AsXML(self): attributes = {'type': self.type, 'interval': self.interval, 'start': self.start} return create_element('Schedule', attributes) + @staticmethod + def CreateFromXML(xml_data): + type = get_attribute(xml_data, 'type') + interval = get_attribute(xml_data, 'interval') + start = get_attribute(xml_data, 'start') + return Schedule(type, interval, start) + class Frequency: def __init__(self, after_scan=False, scheduled=False, schedule=None): """ - :param after_scan: Whether or not to generate after scan completes on any in-scope assets, sites, groups, or tags + :param after_scan: Whether to generate after scan completes on any in-scope assets, sites, groups, or tags :type after_scan: bool - :param scheduled: Whether or not to generate this report on a schedule (cannot be used with after_scan) + :param scheduled: Whether to generate this report on a schedule (cannot be used with after_scan) :type scheduled: bool :param schedule: The schedule for recurring report generation :type schedule: Schedule @@ -282,13 +298,22 @@ def __init__(self, after_scan=False, scheduled=False, schedule=None): self.scheduled = scheduled self.schedule = schedule - def as_xml(self): + def AsXML(self): attributes = {'after-scan': 1 if self.after_scan else 0, 'schedule': 1 if self.schedule else 0} xml_data = create_element('Generate', attributes) if self.schedule: - xml_data.append(self.schedule.as_xml()) + xml_data.append(self.schedule.AsXML()) return xml_data + @staticmethod + def CreateFromXML(xml_data): + after_scan = get_attribute(xml_data, 'after-scan') + schedule_enabled = get_attribute(xml_data, 'schedule') + schedule_xml = get_element(xml_data, 'Schedule') + schedule = Schedule.CreateFromXML(schedule_xml) if schedule_xml is not None else None + truthy = [1, '1', 'true', 'True', 'TRUE'] + return Frequency(after_scan in truthy, schedule_enabled in truthy, schedule) + class Email: def __init__(self, to_all_authorized, send_to_owner_as=None, send_to_acl_as=None, send_as=None): @@ -311,7 +336,7 @@ def __init__(self, to_all_authorized, send_to_owner_as=None, send_to_acl_as=None self.smtp_relay_server = None self.recipients = [] - def as_xml(self): + def AsXML(self): attributes = {'toAllAuthorized': 1 if self.to_all_authorized else 0} if self.send_to_owner_as: attributes['sendToOwnerAs'] = self.send_to_owner_as @@ -342,6 +367,20 @@ def as_xml(self): return xml_data + @staticmethod + def CreateFromXML(xml_data): + to_all_authorized = get_attribute(xml_data, 'toAllAuthorized') + send_to_owner_as = get_attribute(xml_data, 'sendToOwnerAs') + send_to_acl_as = get_attribute(xml_data, 'sendToAclAs') + send_as = get_attribute(xml_data, 'sendAs') + truthy = [1, '1', 'true', 'True', 'TRUE'] + email = Email(to_all_authorized in truthy, send_to_owner_as, send_to_acl_as, send_as) + email.sender = get_content_of(xml_data, 'Sender') + email.smtp_relay_server = get_content_of(xml_data, 'SmtpRelayServer') + recipients = [recipient.text for recipient in get_children_of(xml_data, 'Recipients')] + email.recipients = recipients if recipients else [] + return email + class Delivery: def __init__(self, store_on_server, location=None, email=None): @@ -358,26 +397,37 @@ def __init__(self, store_on_server, location=None, email=None): self.location = location self.email = email - def as_xml(self): + def AsXML(self): xml_data = create_element('Delivery') xml_storage = create_element('Storage', {'storeOnServer': 1 if self.store_on_server else 0}) if self.location: xml_location = create_element('location') xml_location.text = self.location xml_storage.append(xml_location) - + xml_data.append(xml_storage) if self.email: - xml_data.append(self.email.as_xml()) + xml_data.append(self.email.AsXML()) return xml_data + @staticmethod + def CreateFromXML(xml_data): + storage = get_element(xml_data, 'Storage') + store_on_server = get_attribute(storage, 'storeOnServer') + location_xml = get_element(xml_data, 'location') + location = location_xml.text if location_xml is not None else None + email_xml = get_element(xml_data, 'Email') + email = Email.CreateFromXML(email_xml) if email_xml is not None else None + truthy = [1, '1', 'true', 'True', 'TRUE'] + return Delivery(store_on_server in truthy, location, email) + # TODO: implement db export for report config class DBExport: def __init__(self): pass - def as_xml(self): + def AsXML(self): pass @@ -386,5 +436,5 @@ class ExportCredential: def __init__(self): pass - def as_xml(self): + def AsXML(self): pass diff --git a/tests/test_NexposeReportConfigurationSummary.py b/tests/test_NexposeReportConfigurationSummary.py index 356ab49..6e064d2 100644 --- a/tests/test_NexposeReportConfigurationSummary.py +++ b/tests/test_NexposeReportConfigurationSummary.py @@ -13,8 +13,8 @@ def testCreateFromXML(self): fixture = CreateEmptyFixture(XML) report_cfg = ReportConfigurationSummary.CreateFromXML(fixture) self.assertEquals(0, report_cfg.id) - self.assertEquals('', report_cfg.template_id) - self.assertEquals('', report_cfg.name) + self.assertEquals(None, report_cfg.template_id) + self.assertEquals(None, report_cfg.name) self.assertEquals(ReportStatus.UNKNOWN, report_cfg.status) self.assertEquals('', report_cfg.generated_on) self.assertEquals('', report_cfg.URI) From 4f6f6a4540573849b9c5f1a992b0d152c2af0e67 Mon Sep 17 00:00:00 2001 From: Gavin Schneider Date: Tue, 10 Oct 2017 15:04:35 -0700 Subject: [PATCH 4/9] Add support for generating AdhocReportConfiguration --- nexpose/nexpose.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/nexpose/nexpose.py b/nexpose/nexpose.py index 55dfcb7..9c4b7cc 100644 --- a/nexpose/nexpose.py +++ b/nexpose/nexpose.py @@ -24,7 +24,7 @@ from .nexpose_engine import EngineStatus, EnginePriority, EngineBase, EngineSummary, EngineConfiguration from .nexpose_node import NodeScanStatus, NodeBase, Node from .nexpose_privileges import AssetGroupPrivileges, GlobalPrivileges, SitePrivileges -from .nexpose_report import ReportStatus, ReportTemplate, ReportConfigurationSummary, ReportConfiguration, ReportSummary +from .nexpose_report import AdhocReportConfiguration, ReportStatus, ReportTemplate, ReportConfigurationSummary, ReportConfiguration, ReportSummary from .nexpose_role import RoleScope, RoleSummary, RoleDetails from .nexpose_scansummary import VulnerabilityStatus, ScanStatus, ScanSummary, ScanSummaryNodeCounts, ScanSummaryTaskCounts, ScanSummaryVulnerability from .nexpose_site import Host, Range, SiteBase, SiteSummary, SiteConfiguration @@ -624,6 +624,13 @@ def RequestReportDelete(self, report_id, reportconfiguration_id=0): else: return self.ExecuteBasicOnReport("ReportDeleteRequest", report_id) + def RequestAdhocReport(self, report_config): + """ + Generate a new report using the specified report configuration (definition). + This function will return a single ReportGenerateResponse XML object (API 1.1). + """ + return self.ExecuteBasicWithElement('ReportAdhocGenerateRequest', {}, as_xml(report_config)) + def RequestReportAdhocGenerate(self, id): request = """ @@ -633,7 +640,6 @@ def RequestReportAdhocGenerate(self, id): """ return self.ExecuteBasicWithElement("ReportAdhocGenerateRequest", {}, as_xml(request.format(id))) - raise NotImplementedError() # TODO # # The following functions implement the User Management API: @@ -2052,6 +2058,23 @@ def GenerateScanReport(self, scan_or_id): body = ''.join(data[4:-1]) return as_xml(base64.urlsafe_b64decode(body)) + def GenerateAdHocReport(self, adhoc_report_configuration): + """ + Generate adhoc report and return decoded contents. + """ + # TODO: add optional filename param to store the report to disk instead of memory + self._RequireInstanceOf(adhoc_report_configuration, AdhocReportConfiguration) + data = self.RequestAdhocReport(adhoc_report_configuration.AsXML()) + data = self.VerifySuccess(data) + # TODO: figure out a way to handle this safely/correctly for different formats + data = data.tail.replace('\r', '').strip().split('\n') + # assert data[1] == 'Content-Type: text/xml; name=report.xml' + assert data[2] == 'Content-Transfer-Encoding: base64' + assert data[3] == '' + assert data[0] == data[-1][:-2] + body = ''.join(data[4:-1]) + return base64.urlsafe_b64decode(body) + def SaveReportConfiguration(self, report_configuration, generate_now=False): """ Save the configuration of a report and return the id of the saved report config. From 27642c499255787b3cf0e85a4f4c00c0353f4efe Mon Sep 17 00:00:00 2001 From: Gavin Schneider Date: Tue, 10 Oct 2017 15:06:07 -0700 Subject: [PATCH 5/9] Update run_demo.py for general use --- demo/run_demo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/demo/run_demo.py b/demo/run_demo.py index d212acb..f9be183 100755 --- a/demo/run_demo.py +++ b/demo/run_demo.py @@ -698,7 +698,6 @@ def DemonstrateReportAPI(): schedule = Schedule('weekly', 1, "20171105T164239700") freq = Frequency(False, True, schedule) report.frequency = freq - report.owner = 1 report.timezone = 'America/Los_Angeles' print('Saving report configuration...') @@ -760,7 +759,7 @@ def main(): #DemonstrateUserAPI() #DemonstrateAssetGroupAPI() #DemonstrateTicketAPI() - DemonstrateReportAPI() + #DemonstrateReportAPI() #print session.GenerateScanReport(1) From b5970d0a9417500831f5611ab88d1ab3dafc0741 Mon Sep 17 00:00:00 2001 From: Gavin Schneider Date: Tue, 24 Oct 2017 16:51:35 -0700 Subject: [PATCH 6/9] Fix report config loading, add vcr-backed test --- .travis.yml | 2 +- demo/run_demo.py | 1 - nexpose/nexpose.py | 11 +- nexpose/nexpose_report.py | 14 +- pytest.ini | 2 + requirements_test.txt | 2 + .../test_report_config.yaml | 169 ++++++++++++++++++ tests/test_NexposeReport.py | 43 +++++ 8 files changed, 236 insertions(+), 8 deletions(-) create mode 100644 pytest.ini create mode 100644 test_fixtures/cassettes/tests.test_NexposeReport/test_report_config.yaml create mode 100644 tests/test_NexposeReport.py diff --git a/.travis.yml b/.travis.yml index 234bb3d..531ca00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,4 +15,4 @@ install: - pip install -r requirements_test.txt script: - if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then flake8; fi - - py.test tests + - py.test --vcr-record-mode=none tests diff --git a/demo/run_demo.py b/demo/run_demo.py index f9be183..1b08faa 100755 --- a/demo/run_demo.py +++ b/demo/run_demo.py @@ -682,7 +682,6 @@ def DemonstrateReportAPI(): print('----------') # TODO: show off more of the report API functionality than just a single config save/delete - report = nexpose.ReportConfiguration('Python API Client Test Report', 'audit-report', 'raw-xml-v2') report.add_filter('scan', 'last') # this should use site/group/tag filter(s) instead of scan in real world use report.add_common_vuln_filters() # adds vuln filters to match UI defaults diff --git a/nexpose/nexpose.py b/nexpose/nexpose.py index 9c4b7cc..4fb87eb 100644 --- a/nexpose/nexpose.py +++ b/nexpose/nexpose.py @@ -596,7 +596,8 @@ def RequestReportConfig(self, reportconfiguration_id): Retreive the detailed report configuration (definition) of the specified report configuration. This function will return a single ReportConfigResponse XML object (API 1.1). """ - return self.ExecuteBasicOnReportConfiguration("ReportConfigRequest", reportconfiguration_id) + response = self.ExecuteBasicOnReportConfiguration("ReportConfigRequest", reportconfiguration_id) + return get_element(response, 'ReportConfig') def RequestReportSave(self, report_configuration, generate_now=False): """ @@ -2081,9 +2082,13 @@ def SaveReportConfiguration(self, report_configuration, generate_now=False): If successful, the id will also have been updated in the provided ReportConfiguration object. To create a new report, specify -1 as id. """ - # TODO: how to pass generate_now param downstream? self._RequireInstanceOf(report_configuration, ReportConfiguration) - return self._ExecuteSave(self.RequestReportSave, report_configuration, 'ReportSaveResponse', 'reportcfg-id') + response = self.RequestReportSave(report_configuration.AsXML(exclude_id=False), generate_now) + self.VerifySuccess(response) + report_cfg_id = int(get_attribute(response, 'reportcfg-id')) + report_configuration.id = report_cfg_id + return report_cfg_id + # # The following functions implement the Role Management API: diff --git a/nexpose/nexpose_report.py b/nexpose/nexpose_report.py index f64a41e..d2afb07 100644 --- a/nexpose/nexpose_report.py +++ b/nexpose/nexpose_report.py @@ -71,14 +71,14 @@ def __init__(self): class AdhocReportConfiguration(_ReportConfigurationBase): - def __init__(self, template_id, format, site_id=None, owner=None, timezone=None): + def __init__(self, template_id, format, site_id=None, owner=None, timezone=None, language=None, baseline=None): _ReportConfigurationBase.__init__(self, template_id) self.format = format self.owner = owner self.timezone = timezone - self.language = None + self.language = language self.filters = [] - self.baseline = None + self.baseline = baseline if site_id: self.add_filter('site', site_id) @@ -96,6 +96,8 @@ def AsXML(self): attributes['owner'] = self.owner if self.timezone: attributes['timezone'] = self.timezone + if self.language: + attributes['language'] = self.language xml_data = create_element('AdhocReportConfig', attributes) @@ -177,6 +179,8 @@ def AsXML(self, exclude_id=False): attributes['owner'] = self.owner if self.timezone: attributes['timezone'] = self.timezone + if self.language: + attributes['language'] = self.language xml_data = create_element('ReportConfig', attributes) @@ -226,9 +230,13 @@ def __init__(self, type, id): - cyberscope-component - cyberscope-bureau - cyberscope-enclave + - query + - version For site, group, device, tag, and scan the ID is the numeric ID. For scan, the ID can also be "last" for the most recently run scan. + For SQL Query Export, the query value should be the SQL query as a string. The version value should be the + Reporting Data Model version as a string, e.g. '2.3.0'. Both filters must be applied to the report config. For vuln-status, the ID can have one of the following values: 1. vulnerable-exploited (The check was positive. An exploit verified the vulnerability.) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..de19c9f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index cadebfa..42a0af1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,4 +3,6 @@ future>=0.16.0 lxml>=3.6.0 py>=1.4.31 pytest>=2.9.2 +pytest-vcr>=0.3.0 unittest2>=1.1.0; python_version == '2.6' +vcrpy>=1.11.1 diff --git a/test_fixtures/cassettes/tests.test_NexposeReport/test_report_config.yaml b/test_fixtures/cassettes/tests.test_NexposeReport/test_report_config.yaml new file mode 100644 index 0000000..5316e5c --- /dev/null +++ b/test_fixtures/cassettes/tests.test_NexposeReport/test_report_config.yaml @@ -0,0 +1,169 @@ +interactions: +- request: + body: + headers: + Connection: [close] + Content-Length: ['56'] + Content-Type: [text/xml] + Host: ['localhost:3780'] + User-Agent: [Python-urllib/3.6] + method: POST + uri: https://localhost:3780/api/1.1/xml + response: + body: {string: ' + + '} + headers: + Cache-Control: ['no-store, must-revalidate'] + Connection: [close] + Content-Length: ['83'] + Content-Type: [application/xml;charset=UTF-8] + Date: ['Tue, 24 Oct 2017 23:13:44 GMT'] + Server: [Security Console] + Set-Cookie: [nexposeCCSessionID=C949AAFB1DDF91B1865A4471F9E53A79D186B526; Path=/; + Secure; HttpOnly] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-UA-Compatible: ['IE=edge,chrome=1'] + X-XSS-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: whatever@example.comwhatever.example.comsomeone@example.com + headers: + Connection: [close] + Content-Length: ['851'] + Content-Type: [text/xml] + Host: ['localhost:3780'] + User-Agent: [Python-urllib/3.6] + method: POST + uri: https://localhost:3780/api/1.1/xml + response: + body: {string: ' + + '} + headers: + Cache-Control: ['no-store, must-revalidate'] + Connection: [close] + Content-Length: ['54'] + Content-Type: [application/xml;charset=UTF-8] + Date: ['Tue, 24 Oct 2017 23:13:44 GMT'] + Server: [Security Console] + Set-Cookie: [nexposeCCSessionID=A8FCF01B7E5FE8907BD7A9D21BCA48E488E37EE3; Path=/; + Secure; HttpOnly] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-UA-Compatible: ['IE=edge,chrome=1'] + X-XSS-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: + headers: + Connection: [close] + Content-Length: ['96'] + Content-Type: [text/xml] + Host: ['localhost:3780'] + User-Agent: [Python-urllib/3.6] + method: POST + uri: https://localhost:3780/api/1.1/xml + response: + body: {string: ' + + + + + + + + vulnerable-exploited + + vulnerable-version + + potential + + + + + + + + + + + + + + + + + + + + + + someone@example.com + + + + whatever.example.com + + whatever@example.com + + + + + + + + + + '} + headers: + Cache-Control: ['no-store, must-revalidate'] + Connection: [close] + Content-Type: [application/xml;charset=UTF-8] + Date: ['Tue, 24 Oct 2017 23:13:44 GMT'] + Server: [Security Console] + Set-Cookie: [nexposeCCSessionID=09F54BA2C5E305A397AD133D3F1F1B24A79CFD68; Path=/; + Secure; HttpOnly] + Vary: [Accept-Encoding] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-UA-Compatible: ['IE=edge,chrome=1'] + X-XSS-Protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: + headers: + Connection: [close] + Content-Length: ['96'] + Content-Type: [text/xml] + Host: ['localhost:3780'] + User-Agent: [Python-urllib/3.6] + method: POST + uri: https://localhost:3780/api/1.1/xml + response: + body: {string: ' + + '} + headers: + Cache-Control: ['no-store, must-revalidate'] + Connection: [close] + Content-Length: ['36'] + Content-Type: [application/xml;charset=UTF-8] + Date: ['Tue, 24 Oct 2017 23:13:44 GMT'] + Server: [Security Console] + Set-Cookie: [nexposeCCSessionID=E45277AC28B78EF9C7DEAEAD4907E4A889A9B00A; Path=/; + Secure; HttpOnly] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-UA-Compatible: ['IE=edge,chrome=1'] + X-XSS-Protection: [1; mode=block] + status: {code: 200, message: OK} +version: 1 diff --git a/tests/test_NexposeReport.py b/tests/test_NexposeReport.py new file mode 100644 index 0000000..915a90a --- /dev/null +++ b/tests/test_NexposeReport.py @@ -0,0 +1,43 @@ +import os +import pytest +import nexpose.nexpose as nexpose +from nexpose.nexpose_report import Email, Delivery, Frequency, Schedule + + +@pytest.fixture +def vcr_cassette_path(request, vcr_cassette_name): + # Put all cassettes in test_fixtures/cassettes/{module}/{test}.yaml + return os.path.join('test_fixtures', 'cassettes', request.module.__name__, vcr_cassette_name) + + +@pytest.mark.vcr() +def test_report_config(): + # login to Nexpose console + session = nexpose.NexposeSession.Create('localhost', 3780, 'nxadmin', 'nxadmin') + session.Open() + + # create report config object + report = nexpose.ReportConfiguration('Python API Client Test Report', 'audit-report', 'raw-xml-v2') + report.add_filter('scan', 'last') # this should use site/group/tag filter(s) instead of scan in real world use + report.add_common_vuln_filters() # adds vuln filters to match UI defaults + email = Email(True, send_as='file') + email.smtp_relay_server = 'whatever.example.com' + email.sender = 'whatever@example.com' + email.recipients.append('someone@example.com') + delivery = Delivery(True, None, email) + report.delivery = delivery + schedule = Schedule('weekly', 1, "20171105T164239700") + freq = Frequency(False, True, schedule) + report.frequency = freq + report.timezone = 'America/Los_Angeles' + + # save the report config to the console + resp = session.SaveReportConfiguration(report) + assert resp == 4604 # Don't be too sad if/when this changes + + # load the report configuration from the console into a new report config object + loaded_report = nexpose.ReportConfiguration.CreateFromXML(session.RequestReportConfig(resp)) + assert loaded_report.name == report.name + + # finally, delete the report configuration + session.DeleteReportConfiguration(resp) From 8abaa83f9fa7ddd0f236457ccad343e213094710 Mon Sep 17 00:00:00 2001 From: Gavin Schneider Date: Tue, 24 Oct 2017 17:08:44 -0700 Subject: [PATCH 7/9] Fix flake8 errors, remove login request from vcr cassette --- nexpose/nexpose.py | 5 ++-- .../test_report_config.yaml | 28 ------------------- tests/test_NexposeReport.py | 2 +- 3 files changed, 3 insertions(+), 32 deletions(-) diff --git a/nexpose/nexpose.py b/nexpose/nexpose.py index 4fb87eb..4cebbdf 100644 --- a/nexpose/nexpose.py +++ b/nexpose/nexpose.py @@ -284,7 +284,7 @@ def GetSecurityConsoleStatus(self): try: response = OpenWebRequest(self._URI_root, None, {}, self.timeout) return NexposeStatus.GetStatusFromURL(response.geturl()) - except: + except Exception as ex: return NexposeStatus.UNKNOWN @@ -1477,7 +1477,7 @@ def GetAssetGroupConfiguration(self, assetgroup_or_id): asset_group.description = json_dict.get['description'] if asset_group.description is None: asset_group.description = asset_group.short_description - except: + except Exception as ex: pass return asset_group @@ -2089,7 +2089,6 @@ def SaveReportConfiguration(self, report_configuration, generate_now=False): report_configuration.id = report_cfg_id return report_cfg_id - # # The following functions implement the Role Management API: # ========================================================= diff --git a/test_fixtures/cassettes/tests.test_NexposeReport/test_report_config.yaml b/test_fixtures/cassettes/tests.test_NexposeReport/test_report_config.yaml index 5316e5c..4423055 100644 --- a/test_fixtures/cassettes/tests.test_NexposeReport/test_report_config.yaml +++ b/test_fixtures/cassettes/tests.test_NexposeReport/test_report_config.yaml @@ -1,32 +1,4 @@ interactions: -- request: - body: - headers: - Connection: [close] - Content-Length: ['56'] - Content-Type: [text/xml] - Host: ['localhost:3780'] - User-Agent: [Python-urllib/3.6] - method: POST - uri: https://localhost:3780/api/1.1/xml - response: - body: {string: ' - - '} - headers: - Cache-Control: ['no-store, must-revalidate'] - Connection: [close] - Content-Length: ['83'] - Content-Type: [application/xml;charset=UTF-8] - Date: ['Tue, 24 Oct 2017 23:13:44 GMT'] - Server: [Security Console] - Set-Cookie: [nexposeCCSessionID=C949AAFB1DDF91B1865A4471F9E53A79D186B526; Path=/; - Secure; HttpOnly] - X-Content-Type-Options: [nosniff] - X-Frame-Options: [SAMEORIGIN] - X-UA-Compatible: ['IE=edge,chrome=1'] - X-XSS-Protection: [1; mode=block] - status: {code: 200, message: OK} - request: body: Date: Tue, 24 Oct 2017 17:23:54 -0700 Subject: [PATCH 8/9] Add py2/3 compatibility imports for report test --- tests/test_NexposeReport.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_NexposeReport.py b/tests/test_NexposeReport.py index d25562a..3f0d87a 100644 --- a/tests/test_NexposeReport.py +++ b/tests/test_NexposeReport.py @@ -1,7 +1,10 @@ +from __future__ import (absolute_import, division, print_function, unicode_literals) import os import pytest import nexpose.nexpose as nexpose from nexpose.nexpose_report import Email, Delivery, Frequency, Schedule +from future import standard_library +standard_library.install_aliases() @pytest.fixture From 10d4f794fb24d29676d694ac54abe6e9ea3f2cf2 Mon Sep 17 00:00:00 2001 From: Gavin Schneider Date: Wed, 25 Oct 2017 16:56:57 -0700 Subject: [PATCH 9/9] Fix py2 flake8 issues, skip vcr test on py2 Due to the use of python-future for the urllib backports, the vcrpy lib can't wrap/patch the requests in python2 environment. --- nexpose/nexpose.py | 4 ++-- tests/test_NexposeReport.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/nexpose/nexpose.py b/nexpose/nexpose.py index 4cebbdf..cb9bff8 100644 --- a/nexpose/nexpose.py +++ b/nexpose/nexpose.py @@ -284,7 +284,7 @@ def GetSecurityConsoleStatus(self): try: response = OpenWebRequest(self._URI_root, None, {}, self.timeout) return NexposeStatus.GetStatusFromURL(response.geturl()) - except Exception as ex: + except Exception: return NexposeStatus.UNKNOWN @@ -1477,7 +1477,7 @@ def GetAssetGroupConfiguration(self, assetgroup_or_id): asset_group.description = json_dict.get['description'] if asset_group.description is None: asset_group.description = asset_group.short_description - except Exception as ex: + except Exception: pass return asset_group diff --git a/tests/test_NexposeReport.py b/tests/test_NexposeReport.py index 3f0d87a..69953e1 100644 --- a/tests/test_NexposeReport.py +++ b/tests/test_NexposeReport.py @@ -1,10 +1,11 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +from future import standard_library +standard_library.install_aliases() import os +import sys import pytest import nexpose.nexpose as nexpose from nexpose.nexpose_report import Email, Delivery, Frequency, Schedule -from future import standard_library -standard_library.install_aliases() @pytest.fixture @@ -13,6 +14,7 @@ def vcr_cassette_path(request, vcr_cassette_name): return os.path.join('test_fixtures', 'cassettes', request.module.__name__, vcr_cassette_name) +@pytest.mark.skipif(sys.version_info < (3, 4), reason="vcr not working on py2") @pytest.mark.vcr() def test_report_config(): # login to Nexpose console @@ -40,6 +42,7 @@ def test_report_config(): # load the report configuration from the console into a new report config object loaded_report = nexpose.ReportConfiguration.CreateFromXML(session.RequestReportConfig(resp)) + assert isinstance(loaded_report, nexpose.ReportConfiguration) assert loaded_report.name == report.name # finally, delete the report configuration