From eec56cd1e305f96e4c6b61ca616c9e54e8202c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20R=C3=BCchel?= Date: Thu, 14 Dec 2017 13:23:12 +1100 Subject: [PATCH 1/7] Add support for ESO API This mirrors the changes in https://github.com/rapid7/nexpose-client/pull/309 but limited to discovery connections. --- nexpose/nexpose.py | 92 ++++++++++++++++++++++++++++++++ nexpose/nexpose_eso.py | 116 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 nexpose/nexpose_eso.py diff --git a/nexpose/nexpose.py b/nexpose/nexpose.py index c9e70c5..49d05b9 100644 --- a/nexpose/nexpose.py +++ b/nexpose/nexpose.py @@ -24,6 +24,7 @@ from .nexpose_credential import Credential_FTP, Credential_HTTP, Credential_SNMP, Credential_SNMPV3, Credential_SSH, Credential_SSH_KEY, Credential_Telnet from .nexpose_discoveryconnection import DiscoveryConnectionProtocol, DiscoveryConnectionSummary, DiscoveryConnectionConfiguration from .nexpose_engine import EngineStatus, EnginePriority, EngineBase, EngineSummary, EngineConfiguration +from .nexpose_eso import Configuration as EsoConfiguration from .nexpose_node import NodeScanStatus, NodeBase, Node from .nexpose_privileges import AssetGroupPrivileges, GlobalPrivileges, SitePrivileges from .nexpose_report import ReportStatus, ReportTemplate, ReportConfigurationSummary, ReportConfiguration, ReportSummary @@ -142,6 +143,12 @@ def ExecuteDelete_JSON(session_id, uri, sub_url, timeout): return False +def ExecuteDelete_JSON_ESO(session_id, uri, sub_url, timeout): + headers = CreateHeadersWithSessionCookieAndCustomHeader(session_id) + uri = uri + sub_url + return ExecuteWebRequest(uri, None, headers, timeout, lambda: 'DELETE') + + def ExecutePagedGet_JSON(session_id, uri, sub_url, timeout, per_page=2147483647): options = {} options["per_page"] = per_page # NOTA: API 2.0 defaults this to 500 if not set @@ -233,6 +240,8 @@ def DownloadFromStreamReader(reader, callback_function=None, block_size=DEFAULT_ APIURL_ASSETS = "assets/{0}/" APIURL_ASSETGROUPS = "asset_groups/{0}/" +ESOURL_CONFIGMANAGER = "/configuration-manager/api/service/" + class NexposeException(Exception): def __init__(self, message): @@ -271,6 +280,7 @@ def __init__(self, host, port): self._URI_root = BuildURI_root(host, port) self._URI_APIv2d0 = BuildURI_APIv2d0(host, port) self._URI_APIv2d1 = BuildURI_APIv2d1(host, port) + self._URI_APIESO = BuildRootURI(host, port) + 'eso' self.timeout = 60 # @@ -1200,10 +1210,20 @@ def ExecutePagedGet_v20(self, sub_url): def ExecutePagedGet_v21(self, sub_url): return self.ExecutePagedGet_vXX(sub_url, self._URI_APIv2d1) + def ExecutePagedGet_ESO(self, sub_url): + self._RequireAnOpenSession() + result = ExecuteGet_JSON(self._session_id, self._URI_APIESO, sub_url, self.timeout) + return json.loads(result) + def ExecutePost(self, sub_url, post_data): self._RequireAnOpenSession() return ExecutePost_JSON(self._session_id, self._URI_APIv2d0, sub_url, self.timeout, post_data) + def ExecutePost_ESO(self, sub_url, post_data): + self._RequireAnOpenSession() + response = ExecutePost_JSON(self._session_id, self._URI_APIESO, sub_url, self.timeout, post_data) + return json.loads(response) + def ExecutePut(self, sub_url, post_data): self._RequireAnOpenSession() return ExecutePut_JSON(self._session_id, self._URI_APIv2d0, sub_url, self.timeout, post_data) @@ -1212,6 +1232,10 @@ def ExecuteDelete(self, sub_url): self._RequireAnOpenSession() return ExecuteDelete_JSON(self._session_id, self._URI_APIv2d0, sub_url, self.timeout) + def ExecuteDelete_ESO(self, sub_url): + self._RequireAnOpenSession() + return ExecuteDelete_JSON_ESO(self._session_id, self._URI_APIESO, sub_url, self.timeout) + def ExecuteFormPost(self, sub_url, post_data): self._RequireAnOpenSession() return ExecuteWithPostData_FORM(self._session_id, self._URI_root, sub_url, self.timeout, post_data) @@ -2613,3 +2637,71 @@ def DeleteBackup(self, backup_or_filename): backup_or_filename = backup_or_filename.name parameters = {'backupid': backup_or_filename} return self.ExecuteMaintenanceCommand('backupRestore', 'deleteBackup', parameters) + + # + # The following functions implement the ESO Configuration API + # They are used for new-style Discovery connections + # ========================================================================= + + def GetEsoServices(self): + sub_url = ESOURL_CONFIGMANAGER + json_dict = self.ExecutePagedGet_ESO(sub_url) + return json_dict + + def GetEsoConfigurationType(self, service_name): + sub_url = ESOURL_CONFIGMANAGER + 'configurationType/{}'.format(service_name) + json_dict = self.ExecutePagedGet_ESO(sub_url) + return json_dict + + def GetEsoServiceConfigurations(self, service_name): + sub_url = ESOURL_CONFIGMANAGER + 'configuration/{}/'.format(service_name) + json_list = self.ExecutePagedGet_ESO(sub_url) + services = [EsoConfiguration.CreateFromJSON(i) for i in json_list] + return services + + def GetEsoServiceConfigurationByName(self, service_name, config_name): + for config in self.GetEsoServiceConfigurations(service_name): + if config.name == config_name: + return config + else: + raise KeyError("Config {} not found".format(config_name)) + + def GetEsoServiceConfiguration(self, config_or_id): + if isinstance(config_or_id, EsoConfiguration): + config_or_id = config_or_id.id + sub_url = ESOURL_CONFIGMANAGER + 'configuration/id/{}'.format(config_or_id) + json_dict = self.ExecutePagedGet_ESO(sub_url) + return EsoConfiguration.CreateFromJSON(json_dict) + + def SaveEsoServiceConfiguration(self, config): + self._RequireInstanceOf(config, EsoConfiguration) + sub_url = ESOURL_CONFIGMANAGER + 'configuration' + payload = config.as_json() + response = self.ExecutePost_ESO(sub_url, payload) + config_id = int(response['data']) + if config_id < 1: + raise RuntimeError("API returned invalid configID ({}) while attempting to create configuration.".format(config_id)) + config.id = config_id + return config_id + + def DeleteEsoServiceConfiguration(self, config_or_id): + if isinstance(config_or_id, EsoConfiguration): + config_or_id = config_or_id.id + sub_url = ESOURL_CONFIGMANAGER + 'configuration/{}'.format(config_or_id) + result = self.ExecuteDelete_ESO(sub_url) + if result != 'success': + raise RuntimeError("Failed to delete configuration with ID: {}".format(config_or_id)) + + def PreviewEsoServiceConfiguration(self, config): + self._RequireInstanceOf(config, EsoConfiguration) + sub_url = ESOURL_CONFIGMANAGER + 'configuration/preview' + payload = config.as_json() + response_body = self.ExecutePost_ESO(sub_url, payload) + return response_body['previewAssets'] + + def TestEsoServiceConfiguration(self, config): + self._RequireInstanceOf(config, EsoConfiguration) + sub_url = ESOURL_CONFIGMANAGER + 'configuration/test' + payload = config.as_json() + response_body = self.ExecutePost_ESO(sub_url, payload) + return response_body['data'] diff --git a/nexpose/nexpose_eso.py b/nexpose/nexpose_eso.py new file mode 100644 index 0000000..5a7a3a0 --- /dev/null +++ b/nexpose/nexpose_eso.py @@ -0,0 +1,116 @@ +import copy + + +class Configuration(object): + + @classmethod + def CreateFromJSON(cls, json_dict): + services = { + 'amazon-web-services': AWSConfiguration, + } + service_name = json_dict['serviceName'] + if service_name in services: + cls = services[service_name] + config = cls( + service_name=json_dict['serviceName'], + name=json_dict['configName'], + properties=json_dict['configurationAttributes']['properties'], + id=json_dict['configID'], + ) + return config + + def __init__(self, service_name, name, properties, id=None): + self.id = id + self.properties = copy.deepcopy(properties) + self.service_name = service_name + self.name = name + + def as_json(self): + json_dict = { + 'serviceName': self.service_name, + 'configName': self.name, + 'configurationAttributes': { + 'valueClass': 'Object', + 'objectType': 'service_configuration', + 'properties': self.properties, + }, + } + json_dict['configID'] = self.id + + return json_dict + + +class AWSConfiguration(Configuration): + + @property + def region(self): + items = self.properties['region']['items'] + regions = [i['value'] for i in items] + return regions + + @region.setter + def region(self, regions): + items = [{'valueClass': 'String', 'value': r} for r in regions] + self.properties['region']['items'] = items + + def add_region(self, region): + if region in self.region: + raise RuntimeError("Region {} already in configuration".format(region)) + self.properties['region']['items'].append( + {'valueClass': 'String', 'value': region}) + + @property + def engine_inside_aws(self): + return self.properties['engineInsideAWS']['value'] + + @engine_inside_aws.setter + def engine_inside_aws(self, value): + self.properties['engineInsideAWS']['value'] = value + + @property + def import_tags(self): + return self.properties['importTags']['value'] + + @import_tags.setter + def import_tags(self, value): + self.properties['importTags']['value'] = value + + @property + def console_inside_aws(self): + return self.properties['consoleInsideAWS']['value'] + + @console_inside_aws.setter + def console_inside_aws(self, value): + self.properties['consoleInsideAWS']['value'] = value + + @property + def session_name(self): + return self.properties['sessionName']['value'] + + @session_name.setter + def session_name(self, value): + self.properties['sessionName']['value'] = value + + @property + def site_id(self): + return self.properties['siteID']['value'] + + @site_id.setter + def site_id(self, value): + self.properties['siteID']['value'] = str(value) + + @property + def use_proxy(self): + return self.properties['useProxy']['value'] + + @use_proxy.setter + def use_proxy(self, value): + self.properties['useProxy']['value'] = value + + @property + def arn(self): + return self.properties['arn']['value'] + + @arn.setter + def arn(self, value): + self.properties['arn']['value'] = value From 4138e1fb42a8677c3fc89cd3c2df44ea75073530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20R=C3=BCchel?= Date: Tue, 9 Jan 2018 13:58:08 +1100 Subject: [PATCH 2/7] Add support for integration options --- nexpose/nexpose.py | 68 +++++- nexpose/nexpose_eso.py | 303 +++++++++++++++++++++---- nexpose/nexpose_integration_options.py | 153 +++++++++++++ 3 files changed, 472 insertions(+), 52 deletions(-) create mode 100644 nexpose/nexpose_integration_options.py diff --git a/nexpose/nexpose.py b/nexpose/nexpose.py index 49d05b9..016bfe1 100644 --- a/nexpose/nexpose.py +++ b/nexpose/nexpose.py @@ -25,6 +25,7 @@ from .nexpose_discoveryconnection import DiscoveryConnectionProtocol, DiscoveryConnectionSummary, DiscoveryConnectionConfiguration from .nexpose_engine import EngineStatus, EnginePriority, EngineBase, EngineSummary, EngineConfiguration from .nexpose_eso import Configuration as EsoConfiguration +from .nexpose_integration_options import IntegrationOption from .nexpose_node import NodeScanStatus, NodeBase, Node from .nexpose_privileges import AssetGroupPrivileges, GlobalPrivileges, SitePrivileges from .nexpose_report import ReportStatus, ReportTemplate, ReportConfigurationSummary, ReportConfiguration, ReportSummary @@ -79,6 +80,7 @@ def CreateHeadersWithSessionCookie(session_id): def CreateHeadersWithSessionCookieAndCustomHeader(session_id): headers = CreateHeadersWithSessionCookie(session_id) headers["nexposeCCSessionID"] = "{0}".format(session_id) + headers['X-Requested-With'] = 'XMLHttpRequest' return headers @@ -87,7 +89,7 @@ def ExecuteGet_JSON(session_id, uri, sub_url, timeout, options=None): if options is None: options = {} options = ["{0}={1}".format(a[0], a[1]) for a in iter(options.items())] - headers = CreateHeadersWithSessionCookie(session_id) + headers = CreateHeadersWithSessionCookieAndCustomHeader(session_id) # headers["Accept-Encoding"] = "utf-8" if sub_url.startswith('http'): # TODO: refactor uri & sub_url so that json_utils.resolve_urls can work better uri = sub_url @@ -241,6 +243,7 @@ def DownloadFromStreamReader(reader, callback_function=None, block_size=DEFAULT_ APIURL_ASSETGROUPS = "asset_groups/{0}/" ESOURL_CONFIGMANAGER = "/configuration-manager/api/service/" +ESOURL_INTEG_OPTIONS = "/integration-manager-service/api/integration-options/" class NexposeException(Exception): @@ -2673,7 +2676,7 @@ def GetEsoServiceConfiguration(self, config_or_id): json_dict = self.ExecutePagedGet_ESO(sub_url) return EsoConfiguration.CreateFromJSON(json_dict) - def SaveEsoServiceConfiguration(self, config): + def SaveEsoServiceConfiguration(self, config, save_integration_options=False): self._RequireInstanceOf(config, EsoConfiguration) sub_url = ESOURL_CONFIGMANAGER + 'configuration' payload = config.as_json() @@ -2682,11 +2685,20 @@ def SaveEsoServiceConfiguration(self, config): if config_id < 1: raise RuntimeError("API returned invalid configID ({}) while attempting to create configuration.".format(config_id)) config.id = config_id + if save_integration_options: + for opt in config.integration_options: + self.SaveIntegrationOption(opt) + self.StartIntegrationOption(opt) return config_id - def DeleteEsoServiceConfiguration(self, config_or_id): + def DeleteEsoServiceConfiguration(self, config_or_id, delete_integration_options=False): if isinstance(config_or_id, EsoConfiguration): + if delete_integration_options: + for opt in config_or_id.integration_options: + self.DeleteIntegrationOption(opt) config_or_id = config_or_id.id + elif delete_integration_options: + raise TypeError("Need config option if deleting integration options") sub_url = ESOURL_CONFIGMANAGER + 'configuration/{}'.format(config_or_id) result = self.ExecuteDelete_ESO(sub_url) if result != 'success': @@ -2705,3 +2717,53 @@ def TestEsoServiceConfiguration(self, config): payload = config.as_json() response_body = self.ExecutePost_ESO(sub_url, payload) return response_body['data'] + + # + # Implement the integrations manager API + # ========================================================================= + + def GetIntegrationOptions(self): + sub_url = ESOURL_INTEG_OPTIONS + 'options-and-states' + json_list = self.ExecutePagedGet_ESO(sub_url) + integration_options = [ + IntegrationOption.CreateFromJSON(i['integrationOption']) + for i in json_list] + return integration_options + + def SaveIntegrationOption(self, option): + self._RequireInstanceOf(option, IntegrationOption) + sub_url = ESOURL_INTEG_OPTIONS + payload = option.as_json() + response = self.ExecutePost_ESO(sub_url, payload) + option.update(response['data']) + return option.id + + def DeleteIntegrationOption(self, option_or_id): + if isinstance(option_or_id, IntegrationOption): + option_or_id = option_or_id.id + sub_url = ESOURL_INTEG_OPTIONS + '{}/state'.format(option_or_id) + result = json.loads(self.ExecuteDelete_ESO(sub_url)) + msg = result['data'] + if 'being stopped' not in msg: + raise RuntimeError("Failed to delete step with ID: {}: {}".format(option_or_id, msg)) + + def GetIntegrationOption(self, option_id): + for option in self.GetIntegrationOptions(): + if option.id == option_id: + return option + else: + raise KeyError("Option {} not found".format(option_or_id)) + + def GetIntegrationOptionStatus(self, option_or_id): + if isinstance(option_or_id, IntegrationOption): + option_or_id = option_or_id.id + sub_url = ESOURL_INTEG_OPTIONS + '{}/status'.format(option_or_id) + result = self.ExecutePagedGet_ESO(sub_url) + return result['data'] + + def StartIntegrationOption(self, option_or_id): + if isinstance(option_or_id, IntegrationOption): + option_or_id = option_or_id.id + sub_url = ESOURL_INTEG_OPTIONS + '{}/state'.format(option_or_id) + result = self.ExecutePost_ESO(sub_url, {}) + return result['data'] diff --git a/nexpose/nexpose_eso.py b/nexpose/nexpose_eso.py index 5a7a3a0..d7b1141 100644 --- a/nexpose/nexpose_eso.py +++ b/nexpose/nexpose_eso.py @@ -1,30 +1,106 @@ import copy +from .nexpose_integration_options import IntegrationOption, Step, StepConfiguration, ServiceNames class Configuration(object): @classmethod - def CreateFromJSON(cls, json_dict): + def CreateFromJSON(cls, json_dict, integration_options=None): services = { 'amazon-web-services': AWSConfiguration, } service_name = json_dict['serviceName'] + if integration_options is not None: + integration_options = [ + IntegrationOption.CreateFromJSON(i) for i in integration_options] if service_name in services: cls = services[service_name] - config = cls( - service_name=json_dict['serviceName'], - name=json_dict['configName'], - properties=json_dict['configurationAttributes']['properties'], - id=json_dict['configID'], - ) + config = cls( + name=json_dict['configName'], + properties=json_dict['configurationAttributes']['properties'], + id=json_dict['configID'], + integration_options=integration_options, + ) + else: + config = cls( + service_name=json_dict['serviceName'], + name=json_dict['configName'], + properties=json_dict['configurationAttributes']['properties'], + id=json_dict['configID'], + integration_options=integration_options, + ) return config - def __init__(self, service_name, name, properties, id=None): - self.id = id + def __init__(self, service_name, name, properties, steps, id=None): + self._id = id self.properties = copy.deepcopy(properties) self.service_name = service_name self.name = name + @property + def id(self): + return self._id + + @id.setter + def id(self, value): + self._id = id + + def add_property(self, name, prop, type_=None): + if type_ is None: + if isinstance(prop, bool): + type_ = 'Boolean' + elif isinstance(prop, int): + type_ = 'Integer' + elif isinstance(prop, str): + type_ = 'String' + elif isinstance(prop, list): + type_ = 'Array' + else: + raise TypeError("Invalid type {} for prop '{}'".format(type(prop), name)) + data = { + 'valueClass': type_, + } + if type_ == 'Array': + data['items'] = [{'value': v, 'valueClass': 'String'} for v in prop] + else: + data['value'] = prop + self.properties[name] = data + + def get_property(self, name, default=None): + try: + prop = self.properties[name] + except KeyError: + if default is None: + raise + else: + self.add_property(name, default) + prop = self.properties[name] + if self.properties[name]['valueClass'] == 'Array': + prop = [p['value'] for p in prop['items']] + else: + prop = prop['value'] + return prop + + def add_property_item(self, name, item): + if item in self.get_property(name, []): + raise ValueError("{} '{}' already in configuration".format(name, item)) + self.properties[name]['items'].append({'value': item, 'valueClass': 'String'}) + + def _get_step(self, option_name, service_name, type_name=None): + for opt in self._integration_options: + if option_name not in opt.name: + continue + try: + return opt.get_step(service_name, type_name) + except KeyError: + msg = "Unable to find step for option '{}', serviceName '{}'".format( + option_name, service_name) + if type_name is not None: + msg += " and typeName '{}'".format(type_name) + raise KeyError(msg) + else: + raise KeyError("Unknown option_name '{}'".format(option_name)) + def as_json(self): json_dict = { 'serviceName': self.service_name, @@ -35,82 +111,211 @@ def as_json(self): 'properties': self.properties, }, } - json_dict['configID'] = self.id - + if self._id: + json_dict['configID'] = self.id return json_dict + +def _AWS_get_default_verify_opts(): + return IntegrationOption( + name='aws-verify', + steps=[Step( + service_name=ServiceNames.AWS, + config=StepConfiguration(type_name='verify-aws-targets'), + ), Step( + service_name=ServiceNames.NEXPOSE, + config=StepConfiguration( + type_name='verify-external-targets', + previous_type_name='verify-aws-targets', + ), + ), Step( + service_name=ServiceNames.AWS, + config=StepConfiguration( + type_name='verify-aws-targets', + previous_type_name='verify-external-targets', + ), + )], + ) + + +def _AWS_get_default_sync_opts(): + return IntegrationOption( + name='aws-sync', + steps=[Step( + service_name=ServiceNames.AWS, + config=StepConfiguration( + type_name='discover-aws-assets', + ), + ), Step( + service_name=ServiceNames.NEXPOSE, + config=StepConfiguration( + type_name='sync-external-assets', + previous_type_name='discover-aws-assets', + ), + )], + ) + + class AWSConfiguration(Configuration): + service_name = ServiceNames.AWS + + def __init__(self, name, properties=None, integration_options=None, id=None): + if properties is None: + properties = {} + """ + properties = { + 'arn': { + 'value': '', + 'valueClass': 'String', + }, + 'consoleInsideAWS': { + 'value': False, 'valueClass': 'Boolean', + }, + 'engineInsideAWS': { + 'value': False, 'valueClass': 'Boolean', + }, + 'region': { + 'items': [], + 'valueClass': 'Array', + }, + 'sessionName': { + 'value': '', 'valueClass': 'String', + }, + 'useProxy': { + 'value': False, 'valueClass': 'Boolean', + }, + } + """ + if integration_options is None: + integration_options = [] + self._id = None + + self.name = name + self._integration_options = integration_options + self.properties = properties + if id is not None: + self.id = id + + def enable_default_integrations(self, site_id, import_tags=False): + self._integration_options += [ + _AWS_get_default_sync_opts(), + _AWS_get_default_verify_opts(), + ] + self.site_id = site_id + self.import_tags = import_tags + + @property + def id(self): + return self._id + + @id.setter + def id(self, value): + self._id = value + for opt in self._integration_options: + if 'Config ID' not in opt.name: + opt.name = "Config ID:{} - {}".format(value, opt.name) + for step in opt.steps: + if step.service_name != ServiceNames.AWS: + continue + step.config.add_property('discoveryConfigID', value) + + # + # Properties stored directly on AWSConfiguration + # @property - def region(self): - items = self.properties['region']['items'] - regions = [i['value'] for i in items] - return regions + def regions(self): + return self.get_property('region') - @region.setter - def region(self, regions): - items = [{'valueClass': 'String', 'value': r} for r in regions] - self.properties['region']['items'] = items + @regions.setter + def regions(self, regions): + self.add_property('region', list(regions)) def add_region(self, region): - if region in self.region: - raise RuntimeError("Region {} already in configuration".format(region)) - self.properties['region']['items'].append( - {'valueClass': 'String', 'value': region}) + self.add_property_item('region', region) @property def engine_inside_aws(self): - return self.properties['engineInsideAWS']['value'] + return self.get_property('engineInsideAWS') @engine_inside_aws.setter def engine_inside_aws(self, value): - self.properties['engineInsideAWS']['value'] = value - - @property - def import_tags(self): - return self.properties['importTags']['value'] - - @import_tags.setter - def import_tags(self, value): - self.properties['importTags']['value'] = value + self.add_property('engineInsideAWS', bool(value)) @property def console_inside_aws(self): - return self.properties['consoleInsideAWS']['value'] + return self.get_property('consoleInsideAWS') @console_inside_aws.setter def console_inside_aws(self, value): - self.properties['consoleInsideAWS']['value'] = value + self.add_property('consoleInsideAWS', bool(value)) @property def session_name(self): - return self.properties['sessionName']['value'] + return self.get_property('sessionName') @session_name.setter def session_name(self, value): - self.properties['sessionName']['value'] = value - - @property - def site_id(self): - return self.properties['siteID']['value'] - - @site_id.setter - def site_id(self, value): - self.properties['siteID']['value'] = str(value) + self.add_property('sessionName', str(value)) @property def use_proxy(self): - return self.properties['useProxy']['value'] + return self.get_property('useProxy') @use_proxy.setter def use_proxy(self, value): - self.properties['useProxy']['value'] = value + self.add_property('useProxy', bool(value)) @property def arn(self): - return self.properties['arn']['value'] + return self.get_property('arn') @arn.setter def arn(self, value): - self.properties['arn']['value'] = value + self.add_property('arn', str(value)) + + + # + # Properties stored on some steps + # + + @property + def import_tags(self): + step = self._get_step( + 'aws-sync', + 'amazon-web-services', + 'discover-aws-assets', + ) + return step.config.get_property('importTags') + + @import_tags.setter + def import_tags(self, value): + step = self._get_step( + 'aws-sync', + 'amazon-web-services', + 'discover-aws-assets', + ) + step.config.add_property('importTags', bool(value)) + + @property + def site_id(self): + step = self._get_step( + 'aws-sync', + 'nexpose', + 'sync-external-assets', + ) + return step.config.get_property('siteID') + + @site_id.setter + def site_id(self, value): + step = self._get_step( + 'aws-sync', + 'nexpose', + 'sync-external-assets', + ) + return step.config.add_property('siteID', int(value)) + + @property + def integration_options(self): + return self._integration_options diff --git a/nexpose/nexpose_integration_options.py b/nexpose/nexpose_integration_options.py new file mode 100644 index 0000000..204a6c7 --- /dev/null +++ b/nexpose/nexpose_integration_options.py @@ -0,0 +1,153 @@ +class ServiceNames(object): + AWS = 'amazon-web-services' + NEXPOSE = 'nexpose' + + +class Step(object): + + @classmethod + def CreateFromJSON(cls, json_dict): + uuid = json_dict.get('uuid') + service_name = json_dict['serviceName'] + config = StepConfiguration.CreateFromJSON( + json_dict['stepConfiguration']) + return cls( + service_name=service_name, + config=config, + uuid=uuid, + ) + + def __init__(self, service_name, config, uuid=None): + self.uuid = uuid + self.service_name = service_name + self.config = config + + def as_json(self): + data = { + "serviceName": self.service_name, + "stepConfiguration": self.config.as_json(), + } + if self.uuid is not None: + data['uuid'] = self.uuid, + return data + + +class StepConfiguration(object): + + @classmethod + def CreateFromJSON(cls, json_dict): + type_name = json_dict['typeName'] + previous_type_name = json_dict.get('previousTypeName', "") + props = json_dict.get('configurationParams', {}).get('properties', {}) + integration_option_id = json_dict.get('integrationOptionID') + step_config = cls( + type_name, previous_type_name, + set_defaults=False, + integration_option_id=integration_option_id) + step_config.configuration_params['properties'] = props + + def __init__( + self, type_name, previous_type_name="", + set_defaults=True, + workflow_id=None, integration_option_id=None, **properties): + self.type_name = type_name + self.previous_type_name = previous_type_name + if properties is None: + properties = {} + self.configuration_params = { + "valueClass": "Object", + "objectType": "params", + "properties": {}, + } + for prop_name, prop_value in properties.items(): + self.add_property(prop_name, prop_value) + self.workflow_id = workflow_id + self.integration_option_id = integration_option_id + if set_defaults: + self.set_defaults() + + def set_defaults(self): + defaults = {} + if self.type_name == 'discover-aws-assets': + defaults = { + 'excludeAssetsWithTags': '', + 'importTags': False, + 'onlyImportTheseTags': '', + } + for prop_name, default_value in defaults.items(): + if prop_name not in self.configuration_params['properties']: + self.add_property(prop_name, default_value) + + def add_property(self, name, prop, type_=None): + if type_ is None: + if isinstance(prop, bool): + type_ = 'Boolean' + elif isinstance(prop, int): + type_ = 'Integer' + elif isinstance(prop, str): + type_ = 'String' + else: + raise TypeError("Invalid type {} for prop '{}'".format(type(prop), name)) + self.configuration_params['properties'][name] = { + 'valueClass': type_, + 'value': prop, + } + + def get_property(self, name): + return self.configuration_params['properties'][name]['value'] + + def as_json(self): + data = { + "typeName": self.type_name, + "previousTypeName": self.previous_type_name, + "configurationParams": self.configuration_params, + } + if self.workflow_id: + data['workflowID'] = self.workflow_id + if self.integration_option_id: + data['integrationOptionID'] = self.integration_option_id + return data + + +class IntegrationOption(object): + + @classmethod + def CreateFromJSON(cls, json_dict): + name = json_dict.get('name', "") + id_ = json_dict.get('id') + steps = [Step.CreateFromJSON(s) for s in json_dict['steps']] + return cls(name, id_, steps) + + def __init__(self, name="", id=None, steps=None): + if steps is None: + steps = [] + self.name = name + self.id = id + self.steps = steps + + def get_step(self, service_name, type_name=None): + for step in self.steps: + if step.service_name != service_name: + continue + if type_name is not None and step.config.type_name != type_name: + continue + return step + else: + msg = "Unable to find step for serviceName '{}'".format( + service_name) + if type_name is not None: + msg += " and typeName '{}'".format(type_name) + raise KeyError(msg) + + def as_json(self): + data = { + 'name': self.name, + 'steps': [s.as_json() for s in self.steps] + } + if self.id is not None: + data['id'] = self.id + return data + + def update(self, data): + self.id = data['id'] + self.steps = [Step.CreateFromJSON(s) for s in data['steps']] From 52d64b1e7ef2727f5e62aa751acc6edf8b6a7f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20R=C3=BCchel?= Date: Wed, 10 Jan 2018 14:32:30 +1100 Subject: [PATCH 3/7] Fix loading of step config --- nexpose/nexpose_integration_options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nexpose/nexpose_integration_options.py b/nexpose/nexpose_integration_options.py index 204a6c7..f96a20d 100644 --- a/nexpose/nexpose_integration_options.py +++ b/nexpose/nexpose_integration_options.py @@ -45,6 +45,7 @@ def CreateFromJSON(cls, json_dict): set_defaults=False, integration_option_id=integration_option_id) step_config.configuration_params['properties'] = props + return step_config def __init__( self, type_name, previous_type_name="", From 9cc04fcf5273aa16176fed0625f8a1dcf8b96aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20R=C3=BCchel?= Date: Thu, 11 Jan 2018 11:06:06 +1100 Subject: [PATCH 4/7] Remove old, unused code --- nexpose/nexpose_eso.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/nexpose/nexpose_eso.py b/nexpose/nexpose_eso.py index d7b1141..8173c49 100644 --- a/nexpose/nexpose_eso.py +++ b/nexpose/nexpose_eso.py @@ -163,30 +163,6 @@ class AWSConfiguration(Configuration): def __init__(self, name, properties=None, integration_options=None, id=None): if properties is None: properties = {} - """ - properties = { - 'arn': { - 'value': '', - 'valueClass': 'String', - }, - 'consoleInsideAWS': { - 'value': False, 'valueClass': 'Boolean', - }, - 'engineInsideAWS': { - 'value': False, 'valueClass': 'Boolean', - }, - 'region': { - 'items': [], - 'valueClass': 'Array', - }, - 'sessionName': { - 'value': '', 'valueClass': 'String', - }, - 'useProxy': { - 'value': False, 'valueClass': 'Boolean', - }, - } - """ if integration_options is None: integration_options = [] self._id = None From 418040aeb1817be22130a1bc2edd5c333979b5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20R=C3=BCchel?= Date: Thu, 11 Jan 2018 11:06:19 +1100 Subject: [PATCH 5/7] Use new ARN format (multiple values) --- nexpose/nexpose_eso.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nexpose/nexpose_eso.py b/nexpose/nexpose_eso.py index 8173c49..1874489 100644 --- a/nexpose/nexpose_eso.py +++ b/nexpose/nexpose_eso.py @@ -244,12 +244,15 @@ def use_proxy(self, value): self.add_property('useProxy', bool(value)) @property - def arn(self): + def arns(self): return self.get_property('arn') - @arn.setter - def arn(self, value): - self.add_property('arn', str(value)) + @arns.setter + def arns(self, arns): + self.add_property('arn', list(arns)) + + def add_arn(self, arn): + self.add_property_item('arn', arn) # From 009fcf22fcd5eed8e9452439358c74b3fa0d6262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20R=C3=BCchel?= Date: Mon, 19 Feb 2018 13:48:26 +1100 Subject: [PATCH 6/7] Set name to "Schedules" correctly The old value used was "Scheduling" which is not documented anywhere ("Schedule" is the documented value). The parsing function (CreateFromXML) uses "Schedule" as that's what the server sends. This makes the other direction (AsXML) consistent. The server accepts this value. --- nexpose/nexpose_site.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexpose/nexpose_site.py b/nexpose/nexpose_site.py index c2ce326..00007b6 100644 --- a/nexpose/nexpose_site.py +++ b/nexpose/nexpose_site.py @@ -170,7 +170,7 @@ def AsXML(self, exclude_id): attributes['configVersion'] = self.configversion xml_scanconfig = create_element('ScanConfig', attributes) - xml_scheduling = create_element('Scheduling') + xml_scheduling = create_element('Schedules') for schedule in self.schedules: xml_scheduling.append(schedule) xml_scanconfig.append(xml_scheduling) From a6ef422bb75fe92f4858d094d5c1d2b1437be42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20R=C3=BCchel?= Date: Mon, 19 Feb 2018 13:52:15 +1100 Subject: [PATCH 7/7] Add AsXML method for exceptions --- nexpose/nexpose_vulnerabilityexception.py | 31 ++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/nexpose/nexpose_vulnerabilityexception.py b/nexpose/nexpose_vulnerabilityexception.py index 7c359f8..e0fd1b2 100644 --- a/nexpose/nexpose_vulnerabilityexception.py +++ b/nexpose/nexpose_vulnerabilityexception.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, get_content_of +from .xml_utils import get_attribute, get_content_of, create_element from future import standard_library standard_library.install_aliases() @@ -69,6 +69,35 @@ def CreateFromXML(xml_data): details.asset_port = int(fix_null(get_attribute(xml_data, 'port-no', details.asset_port))) return details + def AsXML(self): + attributes = { + 'exception-id': self.id, + 'vuln-id': self.vulnerability_id, + 'vuln-key': self.vulnerability_key, + 'submitter': self.submitter, + 'reviewer': self.reviewer, + 'status': self.status, + 'reason': self.reason, + 'scope': self.scope, + } + if self.expiration_date: + attributes['expiration-date'] = self.expiration_date + if self.asset_id: + attributes['asset-id'] = self.asset_id + if self.asset_port: + attributes['port-no'] = self.asset_port + xml_data = create_element('VulnerabilityException', attributes) + if self.submitter_comment: + submitter_comment = create_element('submitter-comment') + submitter_comment.text = self.submitter_comment + xml_data.append(submitter_comment) + if self.reviewer_comment: + reviewer_comment = create_element('reviewer-comment') + reviewer_comment.text = self.reviewer_comment + xml_data.append(reviewer_comment) + return xml_data + + def __init__(self): self.id = 0 self.vulnerability_id = ''