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)