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 5c7598d..1b08faa 100755 --- a/demo/run_demo.py +++ b/demo/run_demo.py @@ -677,6 +677,37 @@ def DemonstrateTicketAPI(): print(" Comment:", repr(event.comment)) +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_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.timezone = 'America/Los_Angeles' + + print('Saving report configuration...') + resp = session.SaveReportConfiguration(report) + print('Saved Report ID: {}'.format(resp)) + + 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 +751,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..cb9bff8 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 @@ -284,7 +284,7 @@ def GetSecurityConsoleStatus(self): try: response = OpenWebRequest(self._URI_root, None, {}, self.timeout) return NexposeStatus.GetStatusFromURL(response.geturl()) - except: + except Exception: return NexposeStatus.UNKNOWN @@ -596,10 +596,15 @@ 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): - 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): """ @@ -620,6 +625,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 = """ @@ -629,7 +641,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: @@ -1466,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: pass return asset_group @@ -1969,7 +1980,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 +2059,36 @@ 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. + If successful, the id will also have been updated in the provided ReportConfiguration object. + To create a new report, specify -1 as id. + """ + self._RequireInstanceOf(report_configuration, ReportConfiguration) + 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 08a0fab..d2afb07 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 +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() @@ -19,8 +18,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 +34,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 +70,379 @@ 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, language=None, baseline=None): + _ReportConfigurationBase.__init__(self, template_id) + self.format = format + self.owner = owner + self.timezone = timezone + self.language = language + self.filters = [] + self.baseline = baseline + + 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 ['potential', 'vulnerable-version', 'vulnerable-exploited']: + self.add_filter('vuln-status', vuln_filter) + + def AsXML(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 + if self.language: + attributes['language'] = self.language + + xml_data = create_element('AdhocReportConfig', attributes) + + xml_filters = create_element('Filters') + for report_filter in self.filters: + xml_filters.append(report_filter.AsXML()) + 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 + 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') - def __init__(self): - _ReportConfigurationBase.__init__(self) - self.format = '' - self.owner = '' - self.timezone = '' - self.description = '' - self.filters = [] - self.baseline = '' # TODO: default date? + 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 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 __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 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 + if self.timezone: + attributes['timezone'] = self.timezone + if self.language: + attributes['language'] = self.language + + xml_data = create_element('ReportConfig', attributes) + + xml_filters = create_element('Filters') + for report_filter in self.filters: + xml_filters.append(report_filter.AsXML()) + + 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.AsXML()) + + if self.delivery: + xml_data.append(self.delivery.AsXML()) + + 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 + - 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.) + 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 AsXML(self): + 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: + 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 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 to generate after scan completes on any in-scope assets, sites, groups, or tags + :type after_scan: bool + :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 + """ + self.after_scan = after_scan + self.scheduled = scheduled + self.schedule = schedule + + 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.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): + """ + + :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 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 + 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 + + @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): + """ + + :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 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.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 AsXML(self): + pass + + +# TODO: implement db export credential +class ExportCredential: + def __init__(self): + pass + + def AsXML(self): + pass 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..4423055 --- /dev/null +++ b/test_fixtures/cassettes/tests.test_NexposeReport/test_report_config.yaml @@ -0,0 +1,141 @@ +interactions: +- 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..69953e1 --- /dev/null +++ b/tests/test_NexposeReport.py @@ -0,0 +1,49 @@ +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 + + +@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.skipif(sys.version_info < (3, 4), reason="vcr not working on py2") +@pytest.mark.vcr() +def test_report_config(): + # login to Nexpose console + session = nexpose.NexposeSession.Create('localhost', 3780, 'nxadmin', 'nxadmin') + session._session_id = 'C949AAFB1DDF91B1865A4471F9E53A79D186B526' # skip opening a session for real + + # 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 isinstance(loaded_report, nexpose.ReportConfiguration) + assert loaded_report.name == report.name + + # finally, delete the report configuration + session.DeleteReportConfiguration(resp) 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)