From d9e576e6059cca1ae1a45041ea2cc6308a99f52d Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 3 Nov 2020 19:30:09 +0100 Subject: [PATCH 01/10] chg: [farsight_passivedns] Rework of the module to return MISP objects - All the results are parsed as passive-dns MISP objects - More love to give to the parsing to add references between the passive-dns objects and the input attribute, depending on the type of the query (rrset or rdata), or the rrtype (to be determined) --- .../modules/expansion/farsight_passivedns.py | 133 ++++++++++++------ 1 file changed, 92 insertions(+), 41 deletions(-) diff --git a/misp_modules/modules/expansion/farsight_passivedns.py b/misp_modules/modules/expansion/farsight_passivedns.py index 5d32ea8..30a7e9c 100755 --- a/misp_modules/modules/expansion/farsight_passivedns.py +++ b/misp_modules/modules/expansion/farsight_passivedns.py @@ -1,15 +1,76 @@ import json -from ._dnsdb_query.dnsdb_query import DnsdbClient, QueryError - +from ._dnsdb_query.dnsdb_query import DEFAULT_DNSDB_SERVER, DnsdbClient, QueryError +from . import check_input_attribute, standard_error_message +from pymisp import MISPEvent, MISPObject misperrors = {'error': 'Error'} -mispattributes = {'input': ['hostname', 'domain', 'ip-src', 'ip-dst'], 'output': ['freetext']} -moduleinfo = {'version': '0.1', 'author': 'Christophe Vandeplas', 'description': 'Module to access Farsight DNSDB Passive DNS', 'module-type': ['expansion', 'hover']} -moduleconfig = ['apikey'] +mispattributes = { + 'input': ['hostname', 'domain', 'ip-src', 'ip-dst']#, + # 'format': 'misp_standard' +} +moduleinfo = { + 'version': '0.2', + 'author': 'Christophe Vandeplas', + 'description': 'Module to access Farsight DNSDB Passive DNS', + 'module-type': ['expansion', 'hover'] +} +moduleconfig = ['apikey', 'server', 'limit'] -server = 'https://api.dnsdb.info' +DEFAULT_LIMIT = 10 -# TODO return a MISP object with the different attributes + +class FarsightDnsdbParser(): + def __init__(self, attribute): + self.attribute = attribute + self.misp_event = MISPEvent() + self.misp_event.add_attribute(**attribute) + self.passivedns_mapping = { + 'bailiwick': {'type': 'text', 'object_relation': 'bailiwick'}, + 'count': {'type': 'counter', 'object_relation': 'count'}, + 'rdata': {'type': 'text', 'object_relation': 'rdata'}, + 'rrname': {'type': 'text', 'object_relation': 'rrname'}, + 'rrtype': {'type': 'text', 'object_relation': 'rrtype'}, + 'time_first': {'type': 'datetime', 'object_relation': 'time_first'}, + 'time_last': {'type': 'datetime', 'object_relation': 'time_last'}, + 'zone_time_first': {'type': 'datetime', 'object_relation': 'zone_time_first'}, + 'zone_time_last': {'type': 'datetime', 'object_relation': 'zone_time_last'} + } + + def parse_passivedns_results(self, query_response): + default_fields = ('count', 'rrname', 'rrname') + optional_fields = ( + 'bailiwick', + 'time_first', + 'time_last', + 'zone_time_first', + 'zone_time_last' + ) + for result in query_response: + passivedns_object = MISPObject('passive-dns') + for feature in default_fields: + passivedns_object.add_attribute(**self._parse_attribute(feature, result[feature])) + for feature in optional_fields: + if result.get(feature): + passivedns_object.add_attribute(**self._parse_attribute( + feature, + result[feature] + )) + if isinstance(result['rdata'], list): + for rdata in result['rdata']: + passivedns_object.add_attribute(**self._parse_attribute('rdata', rdata)) + else: + passivedns_object.add_attribute(**self._parse_attribute('rdata', result['rdata'])) + self.misp_event.add_object(passivedns_object) + + def get_results(self): + event = json.loads(self.misp_event.to_json()) + results = {key: event[key] for key in ('Attribute', 'Object')} + return {'results': results} + + def _parse_attribute(self, feature, value): + attribute = {'value': value} + attribute.update(self.passivedns_mapping[feature]) + return attribute def handler(q=False): @@ -19,56 +80,46 @@ def handler(q=False): if not request.get('config') or not request['config'].get('apikey'): misperrors['error'] = 'Farsight DNSDB apikey is missing' return misperrors - client = DnsdbClient(server, request['config']['apikey']) - if request.get('hostname'): - res = lookup_name(client, request['hostname']) - elif request.get('domain'): - res = lookup_name(client, request['domain']) - elif request.get('ip-src'): - res = lookup_ip(client, request['ip-src']) - elif request.get('ip-dst'): - res = lookup_ip(client, request['ip-dst']) - else: - misperrors['error'] = "Unsupported attributes type" - return misperrors - - out = '' - for v in set(res): # uniquify entries - out = out + "{} ".format(v) - r = {'results': [{'types': mispattributes['output'], 'values': out}]} - return r + if not request.get('attribute') or not check_input_attribute(request['attribute']): + return {'error': f'{standard_error_message}, which should contain at least a type, a value and an uuid.'} + attribute = request['attribute'] + if attribute['type'] not in mispattributes['input']: + return {'error': 'Unsupported attributes type'} + config = request['config'] + args = {'apikey': config['apikey']} + for feature, default in zip(('server', 'limit'), (DEFAULT_DNSDB_SERVER, DEFAULT_LIMIT)): + args[feature] = config[feature] if config.get(feature) else default + client = DnsdbClient(**args) + to_query = lookup_ip if attribute['type'] in ('ip-src', 'ip-dst') else lookup_name + response = to_query(client, attribute['value']) + if not response: + return {'error': f"Empty results on Farsight DNSDB for the queries {attribute['type']}: {attribute['value']}."} + parser = FarsightDnsdbParser(attribute) + parser.parse_passivedns_results(response) + return parser.get_results() def lookup_name(client, name): try: res = client.query_rrset(name) # RRSET = entries in the left-hand side of the domain name related labels - for item in res: - if item.get('rrtype') in ['A', 'AAAA', 'CNAME']: - for i in item.get('rdata'): - yield(i.rstrip('.')) - if item.get('rrtype') in ['SOA']: - for i in item.get('rdata'): - # grab email field and replace first dot by @ to convert to an email address - yield(i.split(' ')[1].rstrip('.').replace('.', '@', 1)) + response = list(res) except QueryError: - pass - + response = [] try: res = client.query_rdata_name(name) # RDATA = entries on the right-hand side of the domain name related labels - for item in res: - if item.get('rrtype') in ['A', 'AAAA', 'CNAME']: - yield(item.get('rrname').rstrip('.')) + response.extend(list(res)) except QueryError: pass + return response def lookup_ip(client, ip): try: res = client.query_rdata_ip(ip) - for item in res: - yield(item['rrname'].rstrip('.')) + response = list(res) except QueryError: - pass + response = [] + return response def introspection(): From 7c5465e02bec3ebd311c24c3aeb3583a28191ec7 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 4 Nov 2020 18:36:06 +0100 Subject: [PATCH 02/10] fix: [dnsdb] Avoiding AttributeError with the sys library, probably depending on the python version --- misp_modules/modules/expansion/_dnsdb_query/dnsdb_query.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/_dnsdb_query/dnsdb_query.py b/misp_modules/modules/expansion/_dnsdb_query/dnsdb_query.py index af3f204..5df1207 100755 --- a/misp_modules/modules/expansion/_dnsdb_query/dnsdb_query.py +++ b/misp_modules/modules/expansion/_dnsdb_query/dnsdb_query.py @@ -119,7 +119,10 @@ class DnsdbClient(object): break yield json.loads(line.decode('ascii')) except (HTTPError, URLError) as e: - raise QueryError(str(e), sys.exc_traceback) + try: + raise QueryError(str(e), sys.exc_traceback) + except AttributeError: + raise QueryError(str(e), sys.exc_info) def quote(path): From c0440a0d3304a3a945709175987886f3cbe98930 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Wed, 4 Nov 2020 18:37:57 +0100 Subject: [PATCH 03/10] chg: [farsight_passivedns] More context added to the results - References between the passive-dns objects and the initial attribute - Comment on object attributes mentioning whether the results come from an rrset or an rdata lookup --- .../modules/expansion/farsight_passivedns.py | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/misp_modules/modules/expansion/farsight_passivedns.py b/misp_modules/modules/expansion/farsight_passivedns.py index 30a7e9c..7dd47a5 100755 --- a/misp_modules/modules/expansion/farsight_passivedns.py +++ b/misp_modules/modules/expansion/farsight_passivedns.py @@ -35,6 +35,13 @@ class FarsightDnsdbParser(): 'zone_time_first': {'type': 'datetime', 'object_relation': 'zone_time_first'}, 'zone_time_last': {'type': 'datetime', 'object_relation': 'zone_time_last'} } + self.type_to_feature = { + 'domain': 'domain name', + 'hostname': 'hostname', + 'ip-src': 'IP address', + 'ip-dst': 'IP address' + } + self.comment = 'Result from an %s lookup on DNSDB about the %s: %s' def parse_passivedns_results(self, query_response): default_fields = ('count', 'rrname', 'rrname') @@ -45,30 +52,30 @@ class FarsightDnsdbParser(): 'zone_time_first', 'zone_time_last' ) - for result in query_response: - passivedns_object = MISPObject('passive-dns') - for feature in default_fields: - passivedns_object.add_attribute(**self._parse_attribute(feature, result[feature])) - for feature in optional_fields: - if result.get(feature): - passivedns_object.add_attribute(**self._parse_attribute( - feature, - result[feature] - )) - if isinstance(result['rdata'], list): - for rdata in result['rdata']: - passivedns_object.add_attribute(**self._parse_attribute('rdata', rdata)) - else: - passivedns_object.add_attribute(**self._parse_attribute('rdata', result['rdata'])) - self.misp_event.add_object(passivedns_object) + for query_type, results in query_response.items(): + comment = self.comment % (query_type, self.type_to_feature[self.attribute['type']], self.attribute['value']) + for result in results: + passivedns_object = MISPObject('passive-dns') + for feature in default_fields: + passivedns_object.add_attribute(**self._parse_attribute(comment, feature, result[feature])) + for feature in optional_fields: + if result.get(feature): + passivedns_object.add_attribute(**self._parse_attribute(comment, feature, result[feature])) + if isinstance(result['rdata'], list): + for rdata in result['rdata']: + passivedns_object.add_attribute(**self._parse_attribute(comment, 'rdata', rdata)) + else: + passivedns_object.add_attribute(**self._parse_attribute(comment, 'rdata', result['rdata'])) + passivedns_object.add_reference(self.attribute['uuid'], 'related-to') + self.misp_event.add_object(passivedns_object) def get_results(self): event = json.loads(self.misp_event.to_json()) results = {key: event[key] for key in ('Attribute', 'Object')} return {'results': results} - def _parse_attribute(self, feature, value): - attribute = {'value': value} + def _parse_attribute(self, comment, feature, value): + attribute = {'value': value, 'comment': comment} attribute.update(self.passivedns_mapping[feature]) return attribute @@ -100,14 +107,15 @@ def handler(q=False): def lookup_name(client, name): + response = {} try: res = client.query_rrset(name) # RRSET = entries in the left-hand side of the domain name related labels - response = list(res) + response['rrset'] = list(res) except QueryError: - response = [] + pass try: res = client.query_rdata_name(name) # RDATA = entries on the right-hand side of the domain name related labels - response.extend(list(res)) + response['rdata'] = list(res) except QueryError: pass return response @@ -116,9 +124,9 @@ def lookup_name(client, name): def lookup_ip(client, ip): try: res = client.query_rdata_ip(ip) - response = list(res) + response = {'rdata': list(res)} except QueryError: - response = [] + response = {} return response From a357243d314ca3cb7a288e3e0ad441da7b793f62 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 5 Nov 2020 13:06:08 +0100 Subject: [PATCH 04/10] chg: [doc] Updated the farsight_passivedns module documentation --- doc/README.md | 11 ++++++++--- doc/expansion/farsight_passivedns.json | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/doc/README.md b/doc/README.md index f2acc42..3b2bceb 100644 --- a/doc/README.md +++ b/doc/README.md @@ -505,13 +505,18 @@ A module to query the Phishing Initiative service (https://phishing-initiative.l Module to access Farsight DNSDB Passive DNS. - **features**: ->This module takes a domain, hostname or IP address MISP attribute as input to query the Farsight Passive DNS API. The API returns then the result of the query with some information about the value queried. +>This module takes a domain, hostname or IP address MISP attribute as input to query the Farsight Passive DNS API. +> The results of rdata and rrset lookups are then returned and parsed into passive-dns objects. +> +>An API key is required to submit queries to the API. +> It is also possible to define a custom server URL, and to set a limit of results to get. +> This limit is set for each lookup, which means we can have an up to the limit number of passive-dns objects resulting from an rdata query about an IP address, but an up to the limit number of passive-dns objects for each lookup queries about a domain or a hostname (== twice the limit). - **input**: >A domain, hostname or IP address MISP attribute. - **output**: ->Text containing information about the input, resulting from the query on the Farsight Passive DNS API. +>Passive-dns objects, resulting from the query on the Farsight Passive DNS API. - **references**: ->https://www.farsightsecurity.com/ +>https://www.farsightsecurity.com/, https://docs.dnsdb.info/dnsdb-api/ - **requirements**: >An access to the Farsight Passive DNS API (apikey) diff --git a/doc/expansion/farsight_passivedns.json b/doc/expansion/farsight_passivedns.json index 2c1bf05..2dbc64e 100644 --- a/doc/expansion/farsight_passivedns.json +++ b/doc/expansion/farsight_passivedns.json @@ -3,7 +3,7 @@ "logo": "logos/farsight.png", "requirements": ["An access to the Farsight Passive DNS API (apikey)"], "input": "A domain, hostname or IP address MISP attribute.", - "output": "Text containing information about the input, resulting from the query on the Farsight Passive DNS API.", - "references": ["https://www.farsightsecurity.com/"], - "features": "This module takes a domain, hostname or IP address MISP attribute as input to query the Farsight Passive DNS API. The API returns then the result of the query with some information about the value queried." + "output": "Passive-dns objects, resulting from the query on the Farsight Passive DNS API.", + "references": ["https://www.farsightsecurity.com/", "https://docs.dnsdb.info/dnsdb-api/"], + "features": "This module takes a domain, hostname or IP address MISP attribute as input to query the Farsight Passive DNS API.\n The results of rdata and rrset lookups are then returned and parsed into passive-dns objects.\n\nAn API key is required to submit queries to the API.\n It is also possible to define a custom server URL, and to set a limit of results to get.\n This limit is set for each lookup, which means we can have an up to the limit number of passive-dns objects resulting from an rdata query about an IP address, but an up to the limit number of passive-dns objects for each lookup queries about a domain or a hostname (== twice the limit)." } From 87db6f04aa8c443bfae9dc68e521c836af4f119d Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 5 Nov 2020 15:56:01 +0100 Subject: [PATCH 05/10] fix: [tests] Small fixes on the expansion tests --- tests/test_expansions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_expansions.py b/tests/test_expansions.py index 1aa0f7a..eb29332 100644 --- a/tests/test_expansions.py +++ b/tests/test_expansions.py @@ -221,7 +221,7 @@ class TestExpansions(unittest.TestCase): try: self.assertIn(result, self.get_values(response)) except Exception: - self.assertTrue(self.get_errors(response).startwith('Something went wrong')) + self.assertTrue(self.get_errors(response).startswith('Something went wrong')) else: query = {"module": module_name, "ip-src": "8.8.8.8"} response = self.misp_modules_post(query) @@ -285,7 +285,7 @@ class TestExpansions(unittest.TestCase): encoded = b64encode(f.read()).decode() query = {"module": "ocr_enrich", "attachment": filename, "data": encoded} response = self.misp_modules_post(query) - self.assertEqual(self.get_values(response), 'Threat Sharing') + self.assertEqual(self.get_values(response).strip('\n'), 'Threat Sharing') def test_ods(self): filename = 'test.ods' From d9cfcf8f62fcf849557c4864d04596809ed792de Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Thu, 5 Nov 2020 17:51:41 +0100 Subject: [PATCH 06/10] fix: [farsight_passivedns] Uncommented mandatory field that was commented for tests --- misp_modules/modules/expansion/farsight_passivedns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/farsight_passivedns.py b/misp_modules/modules/expansion/farsight_passivedns.py index 7dd47a5..a338bfb 100755 --- a/misp_modules/modules/expansion/farsight_passivedns.py +++ b/misp_modules/modules/expansion/farsight_passivedns.py @@ -5,8 +5,8 @@ from pymisp import MISPEvent, MISPObject misperrors = {'error': 'Error'} mispattributes = { - 'input': ['hostname', 'domain', 'ip-src', 'ip-dst']#, - # 'format': 'misp_standard' + 'input': ['hostname', 'domain', 'ip-src', 'ip-dst'], + 'format': 'misp_standard' } moduleinfo = { 'version': '0.2', From b98562a75eef61aabcc5aa84a00de21682bdc878 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Tue, 10 Nov 2020 17:53:47 +0100 Subject: [PATCH 07/10] chg: [cpe] Support of the new CVE-Search API --- misp_modules/modules/expansion/cpe.py | 28 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/misp_modules/modules/expansion/cpe.py b/misp_modules/modules/expansion/cpe.py index cd5e5fe..bf6f7b6 100644 --- a/misp_modules/modules/expansion/cpe.py +++ b/misp_modules/modules/expansion/cpe.py @@ -6,19 +6,18 @@ from pymisp import MISPEvent, MISPObject misperrors = {'error': 'Error'} mispattributes = {'input': ['cpe'], 'format': 'misp_standard'} moduleinfo = { - 'version': '1', + 'version': '2', 'author': 'Christian Studer', 'description': 'An expansion module to enrich a CPE attribute with its related vulnerabilities.', 'module-type': ['expansion', 'hover'] } moduleconfig = ["custom_API_URL", "limit"] -cveapi_url = 'https://cvepremium.circl.lu/api/cvefor/' +cveapi_url = 'https://cvepremium.circl.lu/api/query' class VulnerabilitiesParser(): - def __init__(self, attribute, api_url): + def __init__(self, attribute): self.attribute = attribute - self.api_url = api_url self.misp_event = MISPEvent() self.misp_event.add_attribute(**attribute) self.vulnerability_mapping = { @@ -100,18 +99,27 @@ def handler(q=False): attribute = request['attribute'] if attribute.get('type') != 'cpe': return {'error': 'Wrong input attribute type.'} - api_url = check_url(request['config']['custom_API_URL']) if request['config'].get('custom_API_URL') else cveapi_url - url = f"{api_url}{attribute['value']}" + url = check_url(request['config']['custom_API_URL']) if request['config'].get('custom_API_URL') else cveapi_url + params = { + "retrieve": "cves", + "dict_filter": { + "vulnerable_configuration": attribute['value'] + } + } if request['config'].get('limit'): - url = f"{url}/{request['config']['limit']}" - response = requests.get(url) + params.update({ + "limit": int(request['config']['limit']), + "sort": "cvss", + "sort_dir": "DESC" + }) + response = requests.post(url, json=params) if response.status_code == 200: - vulnerabilities = response.json() + vulnerabilities = response.json()['data'] if not vulnerabilities: return {'error': 'No related vulnerability for this CPE.'} else: return {'error': 'API not accessible.'} - parser = VulnerabilitiesParser(attribute, api_url) + parser = VulnerabilitiesParser(attribute) parser.parse_vulnerabilities(vulnerabilities) return parser.get_result() From 0650126d6a8117efb03ff9ae2c3f21d1911a442d Mon Sep 17 00:00:00 2001 From: Jesse Hedden Date: Tue, 10 Nov 2020 17:20:03 -0800 Subject: [PATCH 08/10] fixed typo causing firstSeen and lastSeen to not be pulled from enrichment data --- misp_modules/modules/expansion/trustar_enrich.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/trustar_enrich.py b/misp_modules/modules/expansion/trustar_enrich.py index ab472af..1724441 100644 --- a/misp_modules/modules/expansion/trustar_enrich.py +++ b/misp_modules/modules/expansion/trustar_enrich.py @@ -39,7 +39,7 @@ class TruSTARParser: # Relevant fields from each TruSTAR endpoint SUMMARY_FIELDS = ["severityLevel", "source", "score", "attributes"] - METADATA_FIELDS = ["sightings", "first_seen", "last_seen", "tags"] + METADATA_FIELDS = ["sightings", "firstSeen", "lastSeen", "tags"] REPORT_BASE_URL = "https://station.trustar.co/constellation/reports/{}" From bd3fa3ea07d73b60519c5df2b85e7360518cc413 Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Fri, 13 Nov 2020 15:46:41 +0100 Subject: [PATCH 09/10] chg: [cpe] Added default limit to the results - Results returned by CVE-search are sorted by cvss score and limited in number to avoid potential massive amount of data retuned back to MISP. - Users can overwrite the default limit with the configuration already present as optional, and can also set the limit to 0 to get the full list of results --- misp_modules/modules/expansion/cpe.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/misp_modules/modules/expansion/cpe.py b/misp_modules/modules/expansion/cpe.py index bf6f7b6..83cbc46 100644 --- a/misp_modules/modules/expansion/cpe.py +++ b/misp_modules/modules/expansion/cpe.py @@ -13,6 +13,7 @@ moduleinfo = { } moduleconfig = ["custom_API_URL", "limit"] cveapi_url = 'https://cvepremium.circl.lu/api/query' +DEFAULT_LIMIT = 10 class VulnerabilitiesParser(): @@ -99,19 +100,18 @@ def handler(q=False): attribute = request['attribute'] if attribute.get('type') != 'cpe': return {'error': 'Wrong input attribute type.'} - url = check_url(request['config']['custom_API_URL']) if request['config'].get('custom_API_URL') else cveapi_url + config = request['config'] + url = check_url(config['custom_API_URL']) if config.get('custom_API_URL') else cveapi_url + limit = int(config['limit']) if config.get('limit') else DEFAULT_LIMIT params = { "retrieve": "cves", "dict_filter": { "vulnerable_configuration": attribute['value'] - } + }, + "limit": limit, + "sort": "cvss", + "sort_dir": "DESC" } - if request['config'].get('limit'): - params.update({ - "limit": int(request['config']['limit']), - "sort": "cvss", - "sort_dir": "DESC" - }) response = requests.post(url, json=params) if response.status_code == 200: vulnerabilities = response.json()['data'] From 32c0bf9ae28c3b0a5df16b458d5a0f6dd983491f Mon Sep 17 00:00:00 2001 From: chrisr3d Date: Fri, 13 Nov 2020 15:49:58 +0100 Subject: [PATCH 10/10] fix: [cpe] Fixed typo in vulnerable-configuration object relation fields --- misp_modules/modules/expansion/cpe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misp_modules/modules/expansion/cpe.py b/misp_modules/modules/expansion/cpe.py index 83cbc46..600ff37 100644 --- a/misp_modules/modules/expansion/cpe.py +++ b/misp_modules/modules/expansion/cpe.py @@ -32,11 +32,11 @@ class VulnerabilitiesParser(): }, 'vulnerable_configuration': { 'type': 'cpe', - 'object_relation': 'vulnerable_configuration' + 'object_relation': 'vulnerable-configuration' }, 'vulnerable_configuration_cpe_2_2': { 'type': 'cpe', - 'object_relation': 'vulnerable_configuration' + 'object_relation': 'vulnerable-configuration' }, 'Modified': { 'type': 'datetime',