def parse_stix(self, reference=None, make_event=False, source=''): """ Parse the document. :param reference: The reference to the data. :type reference: str :param make_event: Whether or not to create an Event for this document. :type make_event: bool :param source: The source of this document. :type source: str :raises: :class:`crits.standards.parsers.STIXParserException` Until we have a way to map source strings in a STIX document to a source in CRITs, we are being safe and using the source provided as the true source. """ f = StringIO(self.data) self.package = STIXPackage.from_xml(f) f.close() if not self.package: raise STIXParserException("STIX package failure") stix_header = self.package.stix_header if stix_header and stix_header.information_source and stix_header.information_source.identity: self.information_source = stix_header.information_source.identity.name if self.information_source: info_src = "STIX Source: %s" % self.information_source if not reference: reference = '' else: reference += ", " reference += info_src if does_source_exist(source): self.source.name = source elif does_source_exist(self.information_source): self.source.name = self.information_source else: raise STIXParserException("No source to attribute data to.") self.source_instance.reference = reference self.source.instances.append(self.source_instance) if make_event: event = Event.from_stix(stix_package=self.package, source=[self.source]) try: event.save(username=self.source_instance.analyst) self.imported.append((Event._meta['crits_type'], event)) except Exception, e: self.failed.append( (e.message, type(event).__name__, event.id_))
def parse_stix(self, reference=None, make_event=False, source=''): """ Parse the document. :param reference: The reference to the data. :type reference: str :param make_event: Whether or not to create an Event for this document. :type make_event: bool :param source: The source of this document. :type source: str :raises: :class:`crits.standards.parsers.STIXParserException` Until we have a way to map source strings in a STIX document to a source in CRITs, we are being safe and using the source provided as the true source. """ f = StringIO(self.data) self.package = STIXPackage.from_xml(f) f.close() if not self.package: raise STIXParserException("STIX package failure") stix_header = self.package.stix_header if stix_header and stix_header.information_source and stix_header.information_source.identity: self.information_source = stix_header.information_source.identity.name if self.information_source: info_src = "STIX Source: %s" % self.information_source if not reference: reference = '' else: reference += ", " reference += info_src if does_source_exist(source): self.source.name = source elif does_source_exist(self.information_source): self.source.name = self.information_source else: raise STIXParserException("No source to attribute data to.") self.source_instance.reference = reference self.source.instances.append(self.source_instance) if make_event: event = Event.from_stix(stix_package=self.package) try: event.add_source(self.source) event.save(username=self.source_instance.analyst) self.imported.append((Event._meta['crits_type'], event)) except Exception, e: self.failed.append((e.message, type(event).__name__, event.id_))
def _validate(cls, config): hostname = config.get("hostname", "").strip() keyfile = config.get("keyfile", "").strip() certfile = config.get("certfile", "").strip() data_feed = config.get("data_feed", "").strip() certfiles = config.get("certfiles", "") if not hostname: raise ServiceConfigError("You must specify a TAXII Server.") if not keyfile: raise ServiceConfigError("You must specify a keyfile location.") if not os.path.isfile(keyfile): raise ServiceConfigError("keyfile does not exist.") if not certfile: raise ServiceConfigError("You must specify a certfile location.") if not os.path.isfile(certfile): raise ServiceConfigError("certfile does not exist.") if not data_feed: raise ServiceConfigError("You must specify a TAXII Data Feed.") if not certfiles: raise ServiceConfigError("You must specify at least one certfile.") for crtfile in certfiles: try: (source, feed, filepath) = crtfile.split(',') except ValueError: raise ServiceConfigError("You must specify a source, feed name" ", and certificate path for each source.") source.strip() feed.strip() filepath.strip() if not does_source_exist(source): raise ServiceConfigError("Invalid source: %s" % source) if not os.path.isfile(filepath): raise ServiceConfigError("certfile does not exist: %s" % filepath)
def parse_feed_config(config): srv_name = config.get("srv_name", "").strip() feedname = config.get("feedname", "").strip() source = config.get("source", "").strip() subID = config.get("subID", "").strip() fcert = config.get("fcert", "").strip() fkey = config.get("fkey", "").strip() errors = [] if not srv_name: errors.append("No server name to which to relate this feed") if not re.match("^[\w ]+$", srv_name): errors.append("Provided server name is invalid") if not feedname: errors.append("You must specify a Feed Name") if not source: errors.append("You must specify a CRITs source") else: if not does_source_exist(source): errors.append("Provided CRITs source is invalid") if fcert and not os.path.isfile(fcert): errors.append("Encryption Certificate does not exist at given location") if fkey and not os.path.isfile(fkey): errors.append("Decryption Key does not exist at given location") if errors: raise ServiceConfigError("<br>".join(errors))
def parse_feed_config(config): srv_name = config.get("srv_name", "").strip() feedname = config.get("feedname", "").strip() source = config.get("source", "").strip() subID = config.get("subID", "").strip() fcert = config.get("fcert", "").strip() fkey = config.get("fkey", "").strip() errors = [] if not srv_name: errors.append("No server name to which to relate this feed") if not re.match("^[\w ]+$", srv_name): errors.append("Provided server name is invalid") if not feedname: errors.append("You must specify a Feed Name") if not source: errors.append("You must specify a CRITs source") else: if not does_source_exist(source): errors.append("Provided CRITs source is invalid") if fcert and not os.path.isfile(fcert): errors.append( "Encryption Certificate does not exist at given location") if fkey and not os.path.isfile(fkey): errors.append("Decryption Key does not exist at given location") if errors: raise ServiceConfigError("<br>".join(errors))
def _validate(cls, config): hostname = config.get("hostname", "").strip() keyfile = config.get("keyfile", "").strip() certfile = config.get("certfile", "").strip() data_feed = config.get("data_feed", "").strip() certfiles = config.get("certfiles", "") if not hostname: raise ServiceConfigError("You must specify a TAXII Server.") if not keyfile: raise ServiceConfigError("You must specify a keyfile location.") if not os.path.isfile(keyfile): raise ServiceConfigError("keyfile does not exist.") if not certfile: raise ServiceConfigError("You must specify a certfile location.") if not os.path.isfile(certfile): raise ServiceConfigError("certfile does not exist.") if not data_feed: raise ServiceConfigError("You must specify a TAXII Data Feed.") if not certfiles: raise ServiceConfigError("You must specify at least one certfile.") for crtfile in certfiles: try: (source, feed, filepath) = crtfile.split(',') except ValueError: raise ServiceConfigError( ("You must specify a source, feed name" ", and certificate path for each source.")) source.strip() feed.strip() filepath.strip() if not does_source_exist(source): raise ServiceConfigError("Invalid source: %s" % source) if not os.path.isfile(filepath): raise ServiceConfigError("certfile does not exist: %s" % filepath)
def parse_config(config): # When editing a config we are given a string. # When validating an existing config it will be a list. # Convert it to a list of strings. certfiles = config.get('certfiles', []) if isinstance(certfiles, basestring): config['certfiles'] = [cf for cf in certfiles.split('\r\n')] hostname = config.get("hostname", "").strip() keyfile = config.get("keyfile", "").strip() certfile = config.get("certfile", "").strip() data_feed = config.get("data_feed", "").strip() errors = [] if not hostname: errors.append("You must specify a TAXII Server.") if not keyfile: errors.append("You must specify a keyfile location.") if not os.path.isfile(keyfile): errors.append("keyfile does not exist.") if not certfile: errors.append("You must specify a certfile location.") if not os.path.isfile(certfile): errors.append("certfile does not exist.") if not data_feed: errors.append("You must specify a TAXII Data Feed.") if not certfiles: errors.append("You must specify at least one certfile.") if not config.get('polling_time', "").strip().isdigit(): errors.append("Polling time must be an integer.") if not config.get('inbox_time', "").strip().isdigit(): errors.append("Inbox time must be an integer.") for crtfile in config['certfiles']: try: (source, feed, polling, inbox) = crtfile.split(',') except ValueError as e: errors.append( "You must specify a source, feed name, " " true/false polling, and true/false inbox for each source. (%s)" % str(e)) break source.strip() feed.strip() if not does_source_exist(source): errors.append("Invalid source: %s" % source) if errors: raise ServiceConfigError("\n".join(errors))
def parse_config(config): # When editing a config we are given a string. # When validating an existing config it will be a list. # Convert it to a list of strings. certfiles = config.get('certfiles', []) if isinstance(certfiles, basestring): config['certfiles'] = [cf for cf in certfiles.split('\r\n')] hostname = config.get("hostname", "").strip() keyfile = config.get("keyfile", "").strip() certfile = config.get("certfile", "").strip() data_feed = config.get("data_feed", "").strip() errors = [] if not hostname: errors.append("You must specify a TAXII Server.") if not keyfile: errors.append("You must specify a keyfile location.") if not os.path.isfile(keyfile): errors.append("keyfile does not exist.") if not certfile: errors.append("You must specify a certfile location.") if not os.path.isfile(certfile): errors.append("certfile does not exist.") if not data_feed: errors.append("You must specify a TAXII Data Feed.") if not certfiles: errors.append("You must specify at least one certfile.") for crtfile in config['certfiles']: try: (source, feed, filepath) = crtfile.split(',') except ValueError as e: errors.append("You must specify a source, feed name, and " "certificate path for each source. (%s)" % str(e)) break source.strip() feed.strip() filepath.strip() if not does_source_exist(source): errors.append("Invalid source: %s" % source) if not os.path.isfile(filepath): errors.append("certfile does not exist: %s" % filepath) if errors: raise ServiceConfigError("\n".join(errors))
def parse_stix(self, reference='', make_event=False, source=''): """ Parse the document. :param reference: The reference to the data. :type reference: str :param make_event: Whether or not to create an Event for this document. :type make_event: bool :param source: The source of this document. :type source: str :raises: :class:`taxii_service.parsers.STIXParserException` Until we have a way to map source strings in a STIX document to a source in CRITs, we are being safe and using the source provided as the true source. """ f = BytesIO(self.data) self.package = STIXPackage.from_xml(f) f.close() if not self.package: raise STIXParserException("STIX package failure") stix_header = self.package.stix_header if stix_header and stix_header.information_source and stix_header.information_source.identity: self.information_source = stix_header.information_source.identity.name if self.information_source: info_src = "STIX Source: %s" % self.information_source if not reference: reference = '' else: reference += ", " reference += info_src if does_source_exist(source): self.source.name = source elif does_source_exist(self.information_source): self.source.name = self.information_source else: raise STIXParserException("No source to attribute data to.") self.source_instance.reference = reference self.source.instances.append(self.source_instance) if make_event: title = "STIX Document %s" % self.package.id_ event_type = EventTypes.INTEL_SHARING date = datetime.datetime.now() description = str(date) header = self.package.stix_header if isinstance(header, STIXHeader): if header.title: title = header.title if hasattr(header, 'package_intents'): try: stix_type = str(header.package_intents[0]) event_type = get_crits_event_type(stix_type) except: pass if header.description: description = header.description if isinstance(description, StructuredText): try: description = description.to_dict() except: pass res = add_new_event(title, description, event_type, self.source.name, self.source_instance.method, self.source_instance.reference, date, self.source_instance.analyst) if res['success']: self.event = res['object'] self.imported[self.package.id_] = ('Event', res['object']) # Get relationships to the Event if self.package.incidents: incdnts = self.package.incidents for rel in getattr(incdnts[0], 'related_indicators', ()): self.event_rels[rel.item.idref] = ( rel.relationship.value, rel.confidence.value.value) else: self.failed.append((res['message'], "STIX Event", "")) if self.package.indicators: res = self.parse_indicators(self.package.indicators) if res == False: self.parse_campaigns(self.package.indicators, self.package.campaigns) self.parse_ttps(self.package.indicators) self.parse_aliases(self.package.indicators) self.parse_comments(self.package.indicators) self.parse_relationship(self.package.indicators) self.parse_sources(self.package.indicators) self.parse_sectors(self.package.indicators) self.parse_sightings(self.package.indicators) self.parse_kill_chain(self.package.indicators) self.parse_rfi(self.package.indicators) if self.package.campaigns: self.parse_related_campaigns(self.package.indicators, self.package.campaigns) if self.package.stix_header: self.parse_tlp(self.package.indicators, self.package.stix_header) self.set_releasability(self.package.indicators, source) if self.package.observables and self.package.observables.observables: self.parse_observables(self.package.observables.observables) if self.package.threat_actors: self.parse_threat_actors(self.package.threat_actors)
def parse_stix(self, reference=None, make_event=False, source=''): """ Parse the document. :param reference: The reference to the data. :type reference: str :param make_event: Whether or not to create an Event for this document. :type make_event: bool :param source: The source of this document. :type source: str :raises: :class:`crits.standards.parsers.STIXParserException` Until we have a way to map source strings in a STIX document to a source in CRITs, we are being safe and using the source provided as the true source. """ f = StringIO(self.data) (self.package, self.binding) = STIXPackage.from_xml(f) f.close() if not self.package and not self.binding: raise STIXParserException("STIX package failure") stix_header = self.package.stix_header if stix_header and stix_header.information_source and stix_header.information_source.identity: self.information_source = stix_header.information_source.identity.name if self.information_source: info_src = "STIX Source: %s" % self.information_source if not reference: reference = '' else: reference += ", " reference += info_src if does_source_exist(source): self.source.name = source self.source_instance.reference = reference self.source.instances.append(self.source_instance) if make_event: event = Event.from_stix(stix_package=self.package, source=[self.source]) event.save(username=self.source_instance.analyst) self.events.append(('Event', str(event.id))) # Walk STIX indicators and pull out CybOX observables. # stix.(indicators|observables) is a list of CybOX observables if self.package.indicators: for indicator in self.package.indicators: if not indicator: continue for observable in indicator.observables: self.__parse_observable(observable) # Also walk STIX observables and pull out CybOX observables. # At some point the standard will allow stix_package.observables to be # an iterable object and we can collapse this with indicators. if self.package.observables: if self.package.observables.observables: for observable in self.package.observables.observables: if not observable: continue self.__parse_observable(observable)
def parse_stix(self, reference='', make_event=False, source=''): """ Parse the document. :param reference: The reference to the data. :type reference: str :param make_event: Whether or not to create an Event for this document. :type make_event: bool :param source: The source of this document. :type source: str :raises: :class:`taxii_service.parsers.STIXParserException` Until we have a way to map source strings in a STIX document to a source in CRITs, we are being safe and using the source provided as the true source. """ f = BytesIO(self.data) self.package = STIXPackage.from_xml(f) f.close() if not self.package: raise STIXParserException("STIX package failure") stix_header = self.package.stix_header if stix_header and stix_header.information_source and stix_header.information_source.identity: self.information_source = stix_header.information_source.identity.name if self.information_source: info_src = "STIX Source: %s" % self.information_source if not reference: reference = '' else: reference += ", " reference += info_src if does_source_exist(source): self.source.name = source elif does_source_exist(self.information_source): self.source.name = self.information_source else: raise STIXParserException("No source to attribute data to.") self.source_instance.reference = reference self.source.instances.append(self.source_instance) if make_event: title = "STIX Document %s" % self.package.id_ event_type = EventTypes.INTEL_SHARING date = datetime.datetime.now() description = str(date) header = self.package.stix_header if isinstance(header, STIXHeader): if header.title: title = header.title if hasattr(header, 'package_intents'): try: stix_type = str(header.package_intents[0]) event_type = get_crits_event_type(stix_type) except: pass if header.description: description = header.description if isinstance(description, StructuredText): try: description = description.to_dict() except: pass res = add_new_event(title, description, event_type, self.source.name, self.source_instance.method, self.source_instance.reference, date, self.source_instance.analyst) if res['success']: self.event = res['object'] self.imported[self.package.id_] = ('Event', res['object']) # Get relationships to the Event if self.package.incidents: incdnts = self.package.incidents for rel in getattr(incdnts[0], 'related_indicators', ()): self.event_rels[rel.item.idref] = (rel.relationship.value, rel.confidence.value.value) else: self.failed.append((res['message'], "STIX Event", "")) if self.package.indicators: self.parse_indicators(self.package.indicators) if self.package.observables and self.package.observables.observables: self.parse_observables(self.package.observables.observables) if self.package.threat_actors: self.parse_threat_actors(self.package.threat_actors)
def parse_stix(self, reference=None, make_event=False, source=''): """ Parse the document. :param reference: The reference to the data. :type reference: str :param make_event: Whether or not to create an Event for this document. :type make_event: bool :param source: The source of this document. :type source: str :raises: :class:`crits.standards.parsers.STIXParserException` Until we have a way to map source strings in a STIX document to a source in CRITs, we are being safe and using the source provided as the true source. """ f = StringIO(self.data) self.package = STIXPackage.from_xml(f) f.close() if not self.package: raise STIXParserException("STIX package failure") stix_header = self.package.stix_header if stix_header and stix_header.information_source and stix_header.information_source.identity: self.information_source = stix_header.information_source.identity.name if self.information_source: info_src = "STIX Source: %s" % self.information_source if not reference: reference = '' else: reference += ", " reference += info_src if does_source_exist(source): self.source.name = source elif does_source_exist(self.information_source): self.source.name = self.information_source else: raise STIXParserException("No source to attribute data to.") self.source_instance.reference = reference self.source.instances.append(self.source_instance) if make_event: title = "STIX Document %s" % self.package.id_ event_type = "Collective Threat Intelligence" date = datetime.datetime.now() description = str(date) header = self.package.stix_header if isinstance(header, STIXHeader): if header.title: title = header.title if hasattr(header, 'package_intents'): event_type = str(header.package_intents[0]) if header.description: description = header.description if isinstance(description, StructuredText): try: description = description.to_dict() except: pass res = add_new_event(title, description, event_type, self.source.name, self.source_instance.method, self.source_instance.reference, date, self.source_instance.analyst) if res['success']: self.imported.append(('Event', res['object'])) else: self.failed.append((res['message'], "STIX Event", "")) if self.package.indicators: self.parse_indicators(self.package.indicators) if self.package.observables and self.package.observables.observables: self.parse_observables(self.package.observables.observables) if self.package.threat_actors: self.parse_threat_actors(self.package.threat_actors)
def parse_stix(self, reference='', make_event=False, source=''): """ Parse the document. :param reference: The reference to the data. :type reference: str :param make_event: Whether or not to create an Event for this document. :type make_event: bool :param source: The source of this document. :type source: str :raises: :class:`taxii_service.parsers.STIXParserException` Until we have a way to map source strings in a STIX document to a source in CRITs, we are being safe and using the source provided as the true source. """ with closing(StringIO(self.data)) as f: try: try: self.package = STIXPackage.from_xml(f) if not self.package: raise STIXParserException("STIX package failure") except UnsupportedVersionError: v = stix.__version__ v = v[0:-2] if len(v.split('.')) > 3 else v updated = ramrod.update(f, to_=v) doc = updated.document.as_stringio() self.package = STIXPackage.from_xml(doc) except Exception as e: msg = "Failed to create STIX/CybOX from XML" self.failed.append((e.message, "STIX Package (%s)" % msg, '')) # note for display in UI return if not self.preview: self.stix_version = self.package.version stix_header = self.package.stix_header if stix_header and stix_header.information_source and stix_header.information_source.identity: self.information_source = stix_header.information_source.identity.name if self.information_source: info_src = "STIX Source: %s" % self.information_source if not reference: reference = '' else: reference += ", " reference += info_src if source: if does_source_exist(source): self.source.name = source else: raise STIXParserException( 'Source "%s" does not exist in CRITs.' % source) elif does_source_exist(self.information_source): self.source.name = self.information_source else: raise STIXParserException("No source to attribute data to.") self.source_instance.reference = reference self.source.instances.append(self.source_instance) if make_event: title = "STIX Document %s" % self.package.id_ event_type = EventTypes.INTEL_SHARING date = datetime.datetime.now() description = str(date) if self.package.incidents: incdnt = self.package.incidents[0] title = incdnt.title if incdnt.description: description = incdnt.description if isinstance(description, StructuredText): try: description = description.to_dict() except: pass if incdnt.short_description in EventTypes.values(): event_type = incdnt.short_description elif incdnt.categories and incdnt.categories[0].value: event_type = get_crits_event_type( incdnt.categories[0].value) else: #package contains no incidents header = self.package.stix_header if isinstance(header, STIXHeader): if header.title: title = header.title if header.package_intents: try: stix_type = str(header.package_intents[0]) event_type = get_crits_event_type(stix_type) except: pass if header.description: description = header.description if isinstance(description, StructuredText): try: description = description.to_dict() except: pass if self.preview: self.imported[self.package.id_] = ('Event', None, title) else: res = add_new_event(title, description, event_type, self.source.name, self.source_instance.method, self.source_instance.reference, date, self.source_instance.analyst) self.parsed.append(self.package.id_) if res['success']: self.event = res['object'] self.imported[self.package.id_] = ('Event', res['object'].id, title or res['object'].id) self.updates[res['object'].id] = res['object'] # Get relationships to the Event if self.package.incidents: incdnts = self.package.incidents for rel in getattr(incdnts[0], 'related_indicators', ()): if rel.relationship or rel.confidence: r = rel.relationship.value or RelationshipTypes.RELATED_TO c = getattr(rel.confidence.value, 'value', 'Unknown') self.event_rels[rel.item.idref] = (r, c) else: self.failed.append((res['message'], "Event (%s)" % title, self.package.id_)) if self.package.indicators: self.parse_indicators(self.package.indicators) if self.package.observables and self.package.observables.observables: self.parse_observables(self.package.observables.observables) if self.package.threat_actors: self.parse_threat_actors(self.package.threat_actors)
def parse_stix(self, reference='', make_event=False, source=''): """ Parse the document. :param reference: The reference to the data. :type reference: str :param make_event: Whether or not to create an Event for this document. :type make_event: bool :param source: The source of this document. :type source: str :raises: :class:`taxii_service.parsers.STIXParserException` Until we have a way to map source strings in a STIX document to a source in CRITs, we are being safe and using the source provided as the true source. """ with closing(StringIO(self.data)) as f: try: try: self.package = STIXPackage.from_xml(f) if not self.package: raise STIXParserException("STIX package failure") except UnsupportedVersionError: v = stix.__version__ v = v[0:-2] if len(v.split('.')) > 3 else v updated = ramrod.update(f, to_=v) doc = updated.document.as_stringio() self.package = STIXPackage.from_xml(doc) except Exception as e: msg = "Failed to create STIX/CybOX from XML" self.failed.append((e.message, "STIX Package (%s)" % msg, '')) # note for display in UI return if not self.preview: self.stix_version = self.package.version stix_header = self.package.stix_header if stix_header and stix_header.information_source and stix_header.information_source.identity: self.information_source = stix_header.information_source.identity.name if self.information_source: info_src = "STIX Source: %s" % self.information_source if not reference: reference = '' else: reference += ", " reference += info_src if source: if does_source_exist(source): self.source.name = source else: raise STIXParserException('Source "%s" does not exist in CRITs.' % source) elif does_source_exist(self.information_source): self.source.name = self.information_source else: raise STIXParserException("No source to attribute data to.") self.source_instance.reference = reference self.source.instances.append(self.source_instance) if make_event: title = "STIX Document %s" % self.package.id_ event_type = EventTypes.INTEL_SHARING date = datetime.datetime.now() description = str(date) if self.package.incidents: incdnt = self.package.incidents[0] title = incdnt.title if incdnt.description: description = incdnt.description if isinstance(description, StructuredText): try: description = description.to_dict() except: pass if incdnt.short_description in EventTypes.values(): event_type = incdnt.short_description elif incdnt.categories and incdnt.categories[0].value: event_type = get_crits_event_type(incdnt.categories[0].value) else: #package contains no incidents header = self.package.stix_header if isinstance(header, STIXHeader): if header.title: title = header.title if header.package_intents: try: stix_type = str(header.package_intents[0]) event_type = get_crits_event_type(stix_type) except: pass if header.description: description = header.description if isinstance(description, StructuredText): try: description = description.to_dict() except: pass if self.preview: self.imported[self.package.id_] = ('Event', None, title) else: res = add_new_event(title, description, event_type, self.source.name, self.source_instance.method, self.source_instance.reference, date, self.source_instance.analyst) self.parsed.append(self.package.id_) if res['success']: self.event = res['object'] self.imported[self.package.id_] = ('Event', res['object'].id, title or res['object'].id) self.updates[res['object'].id] = res['object'] # Get relationships to the Event if self.package.incidents: incdnts = self.package.incidents for rel in getattr(incdnts[0], 'related_indicators', ()): if rel.relationship or rel.confidence: r = rel.relationship.value or RelationshipTypes.RELATED_TO c = getattr(rel.confidence.value, 'value', 'Unknown') self.event_rels[rel.item.idref] = (r, c) else: self.failed.append((res['message'], "Event (%s)" % title, self.package.id_)) if self.package.indicators: self.parse_indicators(self.package.indicators) if self.package.observables and self.package.observables.observables: self.parse_observables(self.package.observables.observables) if self.package.threat_actors: self.parse_threat_actors(self.package.threat_actors)