diff --git a/internal-enrichment/kaspersky-enrichment/docker-compose.yml b/internal-enrichment/kaspersky-enrichment/docker-compose.yml index 9d78d8110e..ddbbf4e72b 100644 --- a/internal-enrichment/kaspersky-enrichment/docker-compose.yml +++ b/internal-enrichment/kaspersky-enrichment/docker-compose.yml @@ -8,7 +8,7 @@ services: # Common parameters for connectors of type INTERNAL_ENRICHMENT - CONNECTOR_ID=CHANGEME # - CONNECTOR_NAME=Kaspersky Enrichment - # - CONNECTOR_SCOPE=StixFile # Support for additional observable types (IP addresses, domains, and URLs) will be added in future releases. + # - CONNECTOR_SCOPE=StixFile,IPv4-Addr # Support for additional observable types (domains, and URLs) will be added in future releases. # - CONNECTOR_LOG_LEVEL=error # - CONNECTOR_AUTO=true @@ -17,5 +17,6 @@ services: - KASPERSKY_API_KEY=CHANGEME # - KASPERSKY_ZONE_OCTI_SCORE_MAPPING=red:100,orange:80,yellow:60,gray:20,green:0 # - KASPERSKY_FILE_SECTIONS=LicenseInfo,Zone,FileGeneralInfo + # - KASPERSKY_IPV4_SECTIONS=LicenseInfo,Zone,IpGeneralInfo restart: always diff --git a/internal-enrichment/kaspersky-enrichment/src/config.yml.sample b/internal-enrichment/kaspersky-enrichment/src/config.yml.sample index d42d298dc9..5a21addd40 100644 --- a/internal-enrichment/kaspersky-enrichment/src/config.yml.sample +++ b/internal-enrichment/kaspersky-enrichment/src/config.yml.sample @@ -6,7 +6,7 @@ connector: # id: 'ChangeMe' # type: 'INTERNAL_ENRICHMENT' # name: 'Kaspersky Enrichment' -# scope: 'StixFile' # Support for additional observable types (IP addresses, domains, and URLs) will be added in future releases. +# scope: 'StixFile,IPv4-Addr' # Support for additional observable types (domains, and URLs) will be added in future releases. # log_level: 'error' # auto: true # Enable/disable auto-enrichment of observables @@ -15,3 +15,4 @@ kaspersky: api_key: 'ChangeMe' # zone_octi_score_mapping: 'red:100,orange:80,yellow:60,gray:20,green:0' # file_sections: 'LicenseInfo,Zone,FileGeneralInfo' +# ipv4_sections: 'LicenseInfo,Zone,IpGeneralInfo' diff --git a/internal-enrichment/kaspersky-enrichment/src/connector/connector.py b/internal-enrichment/kaspersky-enrichment/src/connector/connector.py index 18d5fbbe53..5593fdb3e9 100644 --- a/internal-enrichment/kaspersky-enrichment/src/connector/connector.py +++ b/internal-enrichment/kaspersky-enrichment/src/connector/connector.py @@ -1,8 +1,10 @@ +from connector.converter_to_stix import ConverterToStix +from connector.settings import ConnectorSettings +from connector.use_cases.enrich_file import FileEnricher +from connector.use_cases.enrich_ipv4 import Ipv4Enricher +from connector.utils import entity_in_scope from kaspersky_client import KasperskyClient -from pycti import STIX_EXT_OCTI_SCO, OpenCTIConnectorHelper, OpenCTIStix2 - -from .converter_to_stix import ConverterToStix -from .settings import ConnectorSettings +from pycti import OpenCTIConnectorHelper class KasperskyConnector: @@ -48,156 +50,40 @@ def __init__( """ self.config = config self.helper = helper - self.file_sections = self.config.kaspersky.file_sections - self.zone_octi_score_mapping = self.config.kaspersky.zone_octi_score_mapping + file_sections = self.config.kaspersky.file_sections + ipv4_sections = self.config.kaspersky.ipv4_sections + zone_octi_score_mapping = self.config.kaspersky.zone_octi_score_mapping api_key = self.config.kaspersky.api_key.get_secret_value() - self.client = KasperskyClient( + client = KasperskyClient( self.helper, base_url=self.config.kaspersky.api_base_url, api_key=api_key, params={ "count": 1, - "sections": self.file_sections, "format": "json", }, ) - self.converter_to_stix = ConverterToStix(self.helper) - - # Define variables - self.stix_objects = [] - - def _process_file(self, observable: dict) -> None: - """ - Collect intelligence from the source for a File type - """ - self.helper.connector_logger.info("[CONNECTOR] Starting enrichment...") - - # Retrieve file hash - obs_hash = self.resolve_file_hash(observable) + converter_to_stix = ConverterToStix(self.helper) - # Get entity data from api client - entity_data = self.client.get_file_info(obs_hash) - - # Check Quota - self.check_quota(entity_data["LicenseInfo"]) - - # Manage FileGeneralInfo data - - self.helper.connector_logger.info( - "[CONNECTOR] Process enrichment from FileGeneralInfo data..." + self.file_enricher = FileEnricher( + helper=self.helper, + client=client, + sections=file_sections, + zone_octi_score_mapping=zone_octi_score_mapping, + converter_to_stix=converter_to_stix, + ) + self.ipv4_enricher = Ipv4Enricher( + helper=self.helper, + client=client, + sections=ipv4_sections, + zone_octi_score_mapping=zone_octi_score_mapping, + converter_to_stix=converter_to_stix, ) - entity_file_general_info = entity_data["FileGeneralInfo"] - - # Score - if entity_data.get("Zone"): - score = self.zone_octi_score_mapping[entity_data["Zone"].lower()] - OpenCTIStix2.put_attribute_in_extension( - observable, STIX_EXT_OCTI_SCO, "score", score - ) - - # Hashes - if entity_file_general_info.get("Md5"): - observable["hashes"]["MD5"] = entity_file_general_info["Md5"] - if entity_file_general_info.get("Sha1"): - observable["hashes"]["SHA-1"] = entity_file_general_info["Sha1"] - if entity_file_general_info.get("Sha256"): - observable["hashes"]["SHA-256"] = entity_file_general_info["Sha256"] - - # Size, mime_type - mapping_fields = {"Size": "size", "Type": "mime_type"} - for key, value in mapping_fields.items(): - if entity_file_general_info.get(key): - observable[value] = entity_file_general_info[key] - - # Labels - if entity_file_general_info.get("Categories"): - observable["labels"] = [] - if observable.get("x_opencti_labels"): - observable["labels"] = observable["x_opencti_labels"] - for label in entity_file_general_info["Categories"]: - if label not in observable["labels"]: - observable["labels"].append(label) - - # Manage FileNames data - - if entity_data.get("FileNames"): - self.helper.connector_logger.info( - "[CONNECTOR] Process enrichment from FileNames data..." - ) - - observable["additional_names"] = observable.get( - "x_opencti_additional_names", [] - ) - for filename in entity_data["FileNames"]: - if filename["FileName"] not in observable["additional_names"]: - observable["additional_names"].append(f" {filename["FileName"]}") - else: - observable["additional_names"] = filename["FileName"] - - # Prepare author object - author = self.converter_to_stix.create_author() - self.stix_objects.append(author) - - # Manage DetectionsInfo data - - if entity_data.get("DetectionsInfo"): - self.helper.connector_logger.info( - "[CONNECTOR] Process enrichment from DetectionsInfo data..." - ) - - content = "| Detection Date | Detection Name | Detection Method |\n" - content += "|----------------|----------------|------------------|\n" - - for obs_detection_info in entity_data["DetectionsInfo"]: - detection_name = f"[{obs_detection_info["DetectionName"]}]({obs_detection_info["DescriptionUrl"]})" - content += f"| {obs_detection_info["LastDetectDate"]} | {detection_name} | {obs_detection_info["DetectionMethod"]} |\n" - - obs_note = self.converter_to_stix.create_file_note( - observable["id"], content - ) - self.stix_objects.append(obs_note) - - # Manage FileDownloadedFromUrls data - - if entity_data.get("FileDownloadedFromUrls"): - self.helper.connector_logger.info( - "[CONNECTOR] Process enrichment from FileDownloadedFromUrls data..." - ) - - for url_info in entity_data["FileDownloadedFromUrls"]: - obs_url_score = self.zone_octi_score_mapping[url_info["Zone"].lower()] - url_object = self.converter_to_stix.create_url(obs_url_score, url_info) - - if url_object: - self.stix_objects.append(url_object) - url_relation = self.converter_to_stix.create_relationship( - source_id=observable["id"], - relationship_type="related-to", - target_id=url_object.id, - ) - self.stix_objects.append(url_relation) - - # Manage Industries data - - if entity_data.get("Industries"): - self.helper.connector_logger.info( - "[CONNECTOR] Process enrichment from Industries data..." - ) - - for industry in entity_data["Industries"]: - industry_object = self.converter_to_stix.create_sector(industry) - - if industry_object: - self.stix_objects.append(industry_object) - industry_relation = self.converter_to_stix.create_relationship( - source_id=observable["id"], - relationship_type="related-to", - target_id=industry_object.id, - ) - self.stix_objects.append(industry_relation) + # Define variables + self.stix_objects = [] def _send_bundle(self, stix_objects: list) -> str: """ @@ -211,46 +97,6 @@ def _send_bundle(self, stix_objects: list) -> str: ) return info_msg - def check_quota(self, entity_info: dict) -> None: - """ - Check if quota is not exceeded. - Raise a warning otherwise. - """ - if entity_info["DayRequests"] >= entity_info["DayQuota"]: - self.helper.connector_logger.warning( - "[CONNECTOR] The daily quota has been exceeded", - { - "day_requests": entity_info["DayRequests"], - "day_quota": entity_info["DayQuota"], - }, - ) - - def entity_in_scope(self, obs_type: str) -> bool: - """ - Security to limit playbook triggers to something other than the initial scope - :param data: Dictionary of data - :return: boolean - """ - scopes = self.helper.connect_scope.lower().replace(" ", "").split(",") - entity_split = obs_type.split("--") - entity_type = entity_split[0].lower() - - if entity_type in scopes: - return True - else: - return False - - def resolve_file_hash(self, observable: dict) -> str: - if "hashes" in observable and "SHA-256" in observable["hashes"]: - return observable["hashes"]["SHA-256"] - if "hashes" in observable and "SHA-1" in observable["hashes"]: - return observable["hashes"]["SHA-1"] - if "hashes" in observable and "MD5" in observable["hashes"]: - return observable["hashes"]["MD5"] - raise ValueError( - "Unable to enrich the observable, the observable does not have an SHA256, SHA1, or MD5" - ) - def process_message(self, data: dict) -> str: """ Get the observable created/modified in OpenCTI and check which type to send for process @@ -272,25 +118,35 @@ def process_message(self, data: dict) -> str: ) self.helper.connector_logger.info(info_msg, {"type": {obs_type}}) - if self.entity_in_scope(obs_type): + if entity_in_scope(self.helper.connect_scope, obs_type): # Performing the collection of intelligence and enrich the entity match obs_type: case "StixFile": - self._process_file(observable) - # case "IPv4-Addr": - # self._process_ip(observable) + octi_objects = self.file_enricher.process_file_enrichment( + observable + ) + case "IPv4-Addr": + octi_objects = self.ipv4_enricher.process_ipv4_enrichment( + observable + ) # case "Domain-Name" | "Hostname": - # self._process_domain(observable) + # octi_objects = self.domain_enricher.process_domain_enrichment( + # observable + # ) # case "Url": - # self._process_url(observable) + # octi_objects = self.url_enricher.process_url_enrichment( + # observable + # ) case _: raise ValueError( "Entity type is not supported", {"entity_type": obs_type}, ) - if self.stix_objects is not None and len(self.stix_objects): - return self._send_bundle(self.stix_objects) + bundle_objects = self.stix_objects + octi_objects + + if bundle_objects is not None and len(bundle_objects): + return self._send_bundle(bundle_objects) else: info_msg = "[CONNECTOR] No information found" return info_msg diff --git a/internal-enrichment/kaspersky-enrichment/src/connector/constants.py b/internal-enrichment/kaspersky-enrichment/src/connector/constants.py new file mode 100644 index 0000000000..4e38302a0b --- /dev/null +++ b/internal-enrichment/kaspersky-enrichment/src/connector/constants.py @@ -0,0 +1,31 @@ +SECTIONS = { + "file_sections": { + "mandatories_sections": ["LicenseInfo", "Zone", "FileGeneralInfo"], + "supported_sections": [ + "LicenseInfo", + "Zone", + "FileGeneralInfo", + "DetectionsInfo", + "FileDownloadedFromUrls", + "Industries", + "FileNames", + ], + }, + "ipv4_sections": { + "mandatories_sections": ["LicenseInfo", "Zone", "IpGeneralInfo"], + "supported_sections": [ + "LicenseInfo", + "Zone", + "IpGeneralInfo", + "FilesDownloadedFromIp", + "HostedUrls", + "IpWhoIs", + "IpDnsResolutions", + "Industries", + ], + }, + "domain_sections": {}, + "url_sections": {}, +} + +DATETIME_FORMAT = "%Y-%m-%dT%H:%MZ" diff --git a/internal-enrichment/kaspersky-enrichment/src/connector/converter_to_stix.py b/internal-enrichment/kaspersky-enrichment/src/connector/converter_to_stix.py index f43dafe619..29beef9750 100644 --- a/internal-enrichment/kaspersky-enrichment/src/connector/converter_to_stix.py +++ b/internal-enrichment/kaspersky-enrichment/src/connector/converter_to_stix.py @@ -1,23 +1,31 @@ import datetime +from typing import Optional -import stix2 -from pycti import Identity, Note, OpenCTIConnectorHelper, StixCoreRelationship +import pytz +from connectors_sdk.models import ( + URL, + AutonomousSystem, + Country, + DomainName, + File, + Note, + OrganizationAuthor, + Reference, + Relationship, + Sector, +) +from pycti import OpenCTIConnectorHelper class ConverterToStix: """ - Provides methods for converting various types of input data into STIX 2.1 objects. - - REQUIREMENTS: - - `generate_id()` methods from `pycti` library MUST be used to generate the `id` of each entity (except observables), - e.g. `pycti.Identity.generate_id(name="Source Name", identity_class="organization")` for a STIX Identity. + Provides methods for converting various types of input data into + STIX 2.1 objects with connectors_sdk models. """ def __init__(self, helper: OpenCTIConnectorHelper) -> None: """ Initialize the converter with necessary configuration. - For log purpose, the connector's helper CAN be injected. - Other arguments CAN be added (e.g. `tlp_level`) if necessary. Args: helper (OpenCTIConnectorHelper): The helper of the connector. Used for logs. @@ -26,73 +34,89 @@ def __init__(self, helper: OpenCTIConnectorHelper) -> None: self.author = self.create_author() @staticmethod - def create_author() -> stix2.Identity: + def create_author() -> OrganizationAuthor: """ Create Author - :return: Author in Stix2 object - """ - author = stix2.Identity( - id=Identity.generate_id( - name="Kaspersky Enrichment", identity_class="organization" - ), - name="Kaspersky Enrichment", - identity_class="organization", - ) + """ + author = OrganizationAuthor(name="Kaspersky Enrichment") return author - def create_file_note(self, obs_id: str, content: str) -> stix2.Note: + def create_autonomous_system(self, number: str) -> AutonomousSystem: + return AutonomousSystem( + number=number, + author=self.author, + ) + + def create_country(self, country_name: str) -> Country: + """ + Create a Country object + """ + return Country( + name=country_name, + ) + + def create_domain(self, name: str, score: int) -> DomainName: + return DomainName(value=name, score=score, author=self.author) + + def create_file(self, hashes: dict, score: int) -> File: + """ + Create a File object + """ + file = File(hashes=hashes, score=score) + return file + + def create_note(self, observable: Reference, content: str) -> Note: """ Create a note associated to the file observable """ - note = stix2.Note( - type="note", - id=Note.generate_id(datetime.datetime.now().isoformat(), content), + return Note( abstract="Kaspersky Detections Info", content=content, - created_by_ref=self.author.id, - object_refs=[obs_id], + objects=[observable], + author=self.author, + publication_date=datetime.datetime.now().astimezone(pytz.UTC), ) - return note - def create_sector(self, industry: str) -> stix2.Identity: + def create_reference(self, obs_id: str) -> Reference: + """ + Create a simple Reference object + """ + return Reference(id=obs_id) + + def create_relationship( + self, + relationship_type: str, + source_obj, + target_obj, + start_time: Optional[str] = None, + stop_time: Optional[str] = None, + ) -> Relationship: + """ + Creates Relationship object + """ + return Relationship( + type=relationship_type, + source=source_obj, + target=target_obj, + author=self.author, + start_time=start_time, + stop_time=stop_time, + ) + + def create_sector(self, industry: str) -> Sector: """ Create a Sector object """ - return stix2.Identity( - id=Identity.generate_id(identity_class="class", name=industry), - identity_class="class", + return Sector( name=industry, - created_by_ref=self.author.id, + author=self.author, ) - def create_url(self, obs_url_score: int, url_info: dict) -> stix2.URL: + def create_url(self, obs_url_score: int, url_info: dict) -> URL: """ Create an URL object """ - return stix2.URL( + return URL( value=url_info["Url"], - custom_properties={ - "score": obs_url_score, - }, - ) - - def create_relationship( - self, source_id: str, relationship_type: str, target_id: str - ) -> stix2.Relationship: - """ - Creates Relationship object - :param source_id: ID of source in string - :param relationship_type: Relationship type in string - :param target_id: ID of target in string - :return: Relationship STIX2 object - """ - relationship = stix2.Relationship( - id=StixCoreRelationship.generate_id( - relationship_type, source_id, target_id - ), - relationship_type=relationship_type, - source_ref=source_id, - target_ref=target_id, - created_by_ref=self.author.id, + score=obs_url_score, ) - return relationship diff --git a/internal-enrichment/kaspersky-enrichment/src/connector/settings.py b/internal-enrichment/kaspersky-enrichment/src/connector/settings.py index b64edee4e9..f742afe129 100644 --- a/internal-enrichment/kaspersky-enrichment/src/connector/settings.py +++ b/internal-enrichment/kaspersky-enrichment/src/connector/settings.py @@ -1,5 +1,6 @@ from typing import Annotated +from connector.constants import SECTIONS from connectors_sdk import ( BaseConfigModel, BaseConnectorSettings, @@ -13,6 +14,7 @@ PlainSerializer, SecretStr, SerializationInfo, + ValidationInfo, field_validator, ) @@ -58,7 +60,7 @@ class InternalEnrichmentConnectorConfig(BaseInternalEnrichmentConnectorConfig): description="Name of the connector.", ) scope: ListFromString = Field( - default=["StixFile"], + default=["StixFile", "IPv4-Addr"], description="The scope or type of data the connector is importing, either a MIME type or Stix Object (for information only).", ) auto: bool = Field( @@ -90,38 +92,36 @@ class KasperskyConfig(BaseConfigModel): file_sections: str = Field( default="LicenseInfo,Zone,FileGeneralInfo", - min_length=1, # Prevent empty string description="Sections wanted to investigate for the requested hash. " "LicenseInfo, Zone and FileGeneralInfo are always set, can't be disabled. " "Only DetectionsInfo, FileDownloadedFromUrls, Industries and FileNames are currently supported", ) + ipv4_sections: str = Field( + default="LicenseInfo,Zone,IpGeneralInfo", + description="Sections wanted to investigate for the requested IPV4. " + "LicenseInfo, Zone and IpGeneralInfo are always set, can't be disabled. " + "Only DetectionsInfo, FileDownloadedFromUrls, Industries and FileNames are currently supported", + ) @field_validator( "file_sections", + "ipv4_sections", mode="before", ) @classmethod - def _validate_value(cls, value: str) -> str: - """Validate the value of the file sections.""" + def _validate_value(cls, value: str, info: ValidationInfo) -> str: + """Validate the value of sections.""" sections = value.replace(" ", "").split(",") + field_constants = SECTIONS[info.field_name] + for section in sections: - if section not in [ - "LicenseInfo", - "Zone", - "FileGeneralInfo", - "DetectionsInfo", - "FileDownloadedFromUrls", - "Industries", - "FileNames", - ]: + if section not in field_constants["supported_sections"]: raise ValueError("Invalid file sections") - for mandatory_section in [ - "LicenseInfo", - "Zone", - "FileGeneralInfo", - ]: + + for mandatory_section in field_constants["mandatories_sections"]: if mandatory_section not in value: value += "," + mandatory_section + return value diff --git a/internal-enrichment/kaspersky-enrichment/src/connector/use_cases/enrich_file.py b/internal-enrichment/kaspersky-enrichment/src/connector/use_cases/enrich_file.py new file mode 100644 index 0000000000..f90f0bf99c --- /dev/null +++ b/internal-enrichment/kaspersky-enrichment/src/connector/use_cases/enrich_file.py @@ -0,0 +1,162 @@ +from connector.converter_to_stix import ConverterToStix +from connector.utils import check_quota, resolve_file_hash +from kaspersky_client import KasperskyClient +from pycti import STIX_EXT_OCTI_SCO, OpenCTIConnectorHelper, OpenCTIStix2 + + +class FileEnricher: + def __init__( + self, + helper: OpenCTIConnectorHelper, + client: KasperskyClient, + sections: str, + zone_octi_score_mapping: dict, + converter_to_stix: ConverterToStix, + ): + self.helper = helper + self.client = client + self.sections = sections + self.zone_octi_score_mapping = zone_octi_score_mapping + self.converter_to_stix = converter_to_stix + + def process_file_enrichment(self, observable: dict) -> list: + """ + Collect intelligence from the source for a File type + """ + octi_objects = [] + observable_to_ref = self.converter_to_stix.create_reference( + obs_id=observable["id"] + ) + self.helper.connector_logger.info("[CONNECTOR] Starting enrichment...") + + # Retrieve file hash + obs_hash = resolve_file_hash(observable) + + # Get entity data from api client + entity_data = self.client.get_file_info(obs_hash, self.sections) + + # Check Quota + if check_quota(entity_data["LicenseInfo"]): + self.helper.connector_logger.warning( + "[CONNECTOR] The daily quota has been exceeded", + { + "day_requests": entity_data["LicenseInfo"]["DayRequests"], + "day_quota": entity_data["LicenseInfo"]["DayQuota"], + }, + ) + + # Manage FileGeneralInfo data + + self.helper.connector_logger.info( + "[CONNECTOR] Process enrichment from FileGeneralInfo data..." + ) + + entity_file_general_info = entity_data["FileGeneralInfo"] + + # Score + if entity_data.get("Zone"): + score = self.zone_octi_score_mapping[entity_data["Zone"].lower()] + OpenCTIStix2.put_attribute_in_extension( + observable, STIX_EXT_OCTI_SCO, "score", score + ) + + # Hashes + if entity_file_general_info.get("Md5"): + observable["hashes"]["MD5"] = entity_file_general_info["Md5"] + if entity_file_general_info.get("Sha1"): + observable["hashes"]["SHA-1"] = entity_file_general_info["Sha1"] + if entity_file_general_info.get("Sha256"): + observable["hashes"]["SHA-256"] = entity_file_general_info["Sha256"] + + # Size, mime_type + mapping_fields = {"Size": "size", "Type": "mime_type"} + for key, value in mapping_fields.items(): + if entity_file_general_info.get(key): + observable[value] = entity_file_general_info[key] + + # Labels + if entity_file_general_info.get("Categories"): + observable["labels"] = [] + if observable.get("x_opencti_labels"): + observable["labels"] = observable["x_opencti_labels"] + for label in entity_file_general_info["Categories"]: + if label not in observable["labels"]: + observable["labels"].append(label) + + # Manage FileNames data + + if entity_data.get("FileNames"): + self.helper.connector_logger.info( + "[CONNECTOR] Process enrichment from FileNames data..." + ) + + observable["additional_names"] = observable.get( + "x_opencti_additional_names", [] + ) + for filename in entity_data["FileNames"]: + if filename["FileName"] not in observable["additional_names"]: + observable["additional_names"].append(f" {filename["FileName"]}") + else: + observable["additional_names"] = filename["FileName"] + + # Prepare author object + author = self.converter_to_stix.create_author() + octi_objects.append(author.to_stix2_object()) + + # Manage DetectionsInfo data + + if entity_data.get("DetectionsInfo"): + self.helper.connector_logger.info( + "[CONNECTOR] Process enrichment from DetectionsInfo data..." + ) + + content = "| Detection Date | Detection Name | Detection Method |\n" + content += "|----------------|----------------|------------------|\n" + + for obs_detection_info in entity_data["DetectionsInfo"]: + detection_name = f"[{obs_detection_info["DetectionName"]}]({obs_detection_info["DescriptionUrl"]})" + content += f"| {obs_detection_info["LastDetectDate"]} | {detection_name} | {obs_detection_info["DetectionMethod"]} |\n" + + obs_note = self.converter_to_stix.create_note(observable_to_ref, content) + octi_objects.append(obs_note.to_stix2_object()) + + # Manage FileDownloadedFromUrls data + + if entity_data.get("FileDownloadedFromUrls"): + self.helper.connector_logger.info( + "[CONNECTOR] Process enrichment from FileDownloadedFromUrls data..." + ) + + for url_info in entity_data["FileDownloadedFromUrls"]: + obs_url_score = self.zone_octi_score_mapping[url_info["Zone"].lower()] + url_object = self.converter_to_stix.create_url(obs_url_score, url_info) + + if url_object: + octi_objects.append(url_object.to_stix2_object()) + url_relation = self.converter_to_stix.create_relationship( + relationship_type="related-to", + source_obj=observable_to_ref, + target_obj=url_object, + ) + octi_objects.append(url_relation.to_stix2_object()) + + # Manage Industries data + + if entity_data.get("Industries"): + self.helper.connector_logger.info( + "[CONNECTOR] Process enrichment from Industries data..." + ) + + for industry in entity_data["Industries"]: + industry_object = self.converter_to_stix.create_sector(industry) + + if industry_object: + octi_objects.append(industry_object.to_stix2_object()) + industry_relation = self.converter_to_stix.create_relationship( + relationship_type="related-to", + source_obj=observable_to_ref, + target_obj=industry_object, + ) + octi_objects.append(industry_relation.to_stix2_object()) + + return octi_objects diff --git a/internal-enrichment/kaspersky-enrichment/src/connector/use_cases/enrich_ipv4.py b/internal-enrichment/kaspersky-enrichment/src/connector/use_cases/enrich_ipv4.py new file mode 100644 index 0000000000..0d5ff86a8b --- /dev/null +++ b/internal-enrichment/kaspersky-enrichment/src/connector/use_cases/enrich_ipv4.py @@ -0,0 +1,219 @@ +from datetime import timedelta + +from connector.constants import DATETIME_FORMAT +from connector.converter_to_stix import ConverterToStix +from connector.utils import ( + check_quota, + is_last_seen_equal_to_first_seen, + string_to_datetime, +) +from kaspersky_client import KasperskyClient +from pycti import STIX_EXT_OCTI_SCO, OpenCTIConnectorHelper, OpenCTIStix2 + + +class Ipv4Enricher: + def __init__( + self, + helper: OpenCTIConnectorHelper, + client: KasperskyClient, + sections: str, + zone_octi_score_mapping: dict, + converter_to_stix: ConverterToStix, + ): + self.helper = helper + self.client = client + self.sections = sections + self.zone_octi_score_mapping = zone_octi_score_mapping + self.converter_to_stix = converter_to_stix + + def process_ipv4_enrichment(self, observable: dict) -> list: + """ + Collect intelligence from the source for an IPV4 type + """ + octi_objects = [] + observable_to_ref = self.converter_to_stix.create_reference( + obs_id=observable["id"] + ) + self.helper.connector_logger.info("[CONNECTOR] Starting enrichment...") + + # Retrieve ipv4 + obs_ipv4 = observable["value"] + + # Get entity data from api client + entity_data = self.client.get_ipv4_info(obs_ipv4, self.sections) + + # Check Quota + if check_quota(entity_data["LicenseInfo"]): + self.helper.connector_logger.warning( + "[CONNECTOR] The daily quota has been exceeded", + { + "day_requests": entity_data["LicenseInfo"]["DayRequests"], + "day_quota": entity_data["LicenseInfo"]["DayQuota"], + }, + ) + + # Manage IpGeneralInfo data + + self.helper.connector_logger.info( + "[CONNECTOR] Process enrichment from IpGeneralInfo data..." + ) + entity_general_info = entity_data["IpGeneralInfo"] + + # Score + if entity_data.get("Zone"): + score = self.zone_octi_score_mapping[entity_data["Zone"].lower()] + OpenCTIStix2.put_attribute_in_extension( + observable, STIX_EXT_OCTI_SCO, "score", score + ) + + # Labels + if entity_general_info.get("Categories"): + observable["labels"] = [] + if observable.get("x_opencti_labels"): + observable["labels"] = observable["x_opencti_labels"] + for label in entity_general_info["Categories"]: + if label not in observable["labels"]: + observable["labels"].append(label) + + # Country + if entity_general_info.get("CountryCode"): + obs_country = self.converter_to_stix.create_country( + entity_general_info["CountryCode"] + ) + + if obs_country: + octi_objects.append(obs_country.to_stix2_object()) + country_relation = self.converter_to_stix.create_relationship( + source_obj=observable_to_ref, + relationship_type="located-at", + target_obj=obs_country, + ) + octi_objects.append(country_relation.to_stix2_object()) + + # Manage FilesDownloadedFromIp data + + self.helper.connector_logger.info( + "[CONNECTOR] Process enrichment from FilesDownloadedFromIp data..." + ) + + if entity_data.get("FilesDownloadedFromIp"): + for file in entity_data["FilesDownloadedFromIp"]: + obs_file = self.converter_to_stix.create_file( + hashes={"MD5": file["Md5"]}, + score=self.zone_octi_score_mapping[file["Zone"].lower()], + ) + + if obs_file: + octi_objects.append(obs_file.to_stix2_object()) + file_first_seen_datetime, file_last_seen_datetime = ( + self.get_first_and_last_seen_datetime( + file["FirstSeen"], file["LastSeen"] + ) + ) + file_relation = self.converter_to_stix.create_relationship( + relationship_type="related-to", + source_obj=observable_to_ref, + target_obj=obs_file, + start_time=file_first_seen_datetime, + stop_time=file_last_seen_datetime, + ) + octi_objects.append(file_relation.to_stix2_object()) + + # Manage HostedUrls data + + if entity_data.get("HostedUrls"): + for url_entity in entity_data["HostedUrls"]: + obs_url = self.converter_to_stix.create_url( + url_info=url_entity, + obs_url_score=self.zone_octi_score_mapping[ + url_entity["Zone"].lower() + ], + ) + + if obs_url: + octi_objects.append(obs_url.to_stix2_object()) + url_first_seen_datetime, url_last_seen_datetime = ( + self.get_first_and_last_seen_datetime( + url_entity["FirstSeen"], url_entity["LastSeen"] + ) + ) + url_relation = self.converter_to_stix.create_relationship( + relationship_type="related-to", + source_obj=observable_to_ref, + target_obj=obs_url, + start_time=url_first_seen_datetime, + stop_time=url_last_seen_datetime, + ) + octi_objects.append(url_relation.to_stix2_object()) + + # Manage IpWhoIs data + + if entity_data.get("IpWhoIs") and entity_data["IpWhoIs"].get("Asn"): + asn_entities = entity_data["IpWhoIs"]["Asn"] + for asn_entity in asn_entities: + obs_asn = self.converter_to_stix.create_autonomous_system( + number=asn_entity["Number"] + ) + + if obs_asn: + octi_objects.append(obs_asn.to_stix2_object()) + asn_relation = self.converter_to_stix.create_relationship( + source_obj=observable_to_ref, + relationship_type="belongs-to", + target_obj=obs_asn, + ) + octi_objects.append(asn_relation.to_stix2_object()) + + # Manage IpDnsResolutions + + if entity_data.get("IpDnsResolutions"): + for resolution in entity_data["IpDnsResolutions"]: + obs_domain = self.converter_to_stix.create_domain( + name=resolution["Domain"], + score=self.zone_octi_score_mapping[resolution["Zone"].lower()], + ) + + if obs_domain: + octi_objects.append(obs_domain.to_stix2_object()) + domain_first_seen_datetime, domain_last_seen_datetime = ( + self.get_first_and_last_seen_datetime( + resolution["FirstSeen"], resolution["LastSeen"] + ) + ) + domain_relation = self.converter_to_stix.create_relationship( + relationship_type="resolves-to", + source_obj=obs_domain, + target_obj=observable_to_ref, + start_time=domain_first_seen_datetime, + stop_time=domain_last_seen_datetime, + ) + octi_objects.append(domain_relation.to_stix2_object()) + + # Manage Industries data + + if entity_data.get("Industries"): + for industry in entity_data["Industries"]: + industry_object = self.converter_to_stix.create_sector(industry) + + if industry_object: + octi_objects.append(industry_object.to_stix2_object()) + industry_relation = self.converter_to_stix.create_relationship( + relationship_type="related-to", + source_obj=observable_to_ref, + target_obj=industry_object, + ) + octi_objects.append(industry_relation.to_stix2_object()) + + return octi_objects + + def get_first_and_last_seen_datetime(self, first_seen, last_seen): + """ + Convert first and last seen string to datetime. + If last==first, add one minute to last seen value. + """ + first_seen_datetime = string_to_datetime(first_seen, DATETIME_FORMAT) + last_seen_datetime = string_to_datetime(last_seen, DATETIME_FORMAT) + if is_last_seen_equal_to_first_seen(first_seen_datetime, last_seen_datetime): + last_seen_datetime = last_seen_datetime + timedelta(minutes=1) + + return first_seen_datetime, last_seen_datetime diff --git a/internal-enrichment/kaspersky-enrichment/src/connector/utils.py b/internal-enrichment/kaspersky-enrichment/src/connector/utils.py new file mode 100644 index 0000000000..b50c8524b9 --- /dev/null +++ b/internal-enrichment/kaspersky-enrichment/src/connector/utils.py @@ -0,0 +1,48 @@ +from datetime import datetime, timezone + + +def check_quota(entity_info: dict) -> bool: + """ + Return True if quota is exceeded. + """ + if entity_info["DayRequests"] >= entity_info["DayQuota"]: + return True + return False + + +def entity_in_scope(connect_scope, obs_type: str) -> bool: + """ + Security to limit playbook triggers to something other than the initial scope + :param data: Dictionary of data + :return: boolean + """ + scopes = connect_scope.lower().replace(" ", "").split(",") + entity_split = obs_type.split("--") + entity_type = entity_split[0].lower() + + if entity_type in scopes: + return True + else: + return False + + +def resolve_file_hash(observable: dict) -> str: + if "hashes" in observable and "SHA-256" in observable["hashes"]: + return observable["hashes"]["SHA-256"] + if "hashes" in observable and "SHA-1" in observable["hashes"]: + return observable["hashes"]["SHA-1"] + if "hashes" in observable and "MD5" in observable["hashes"]: + return observable["hashes"]["MD5"] + raise ValueError( + "Unable to enrich the observable, the observable does not have an SHA256, SHA1, or MD5" + ) + + +def string_to_datetime(value: str, format: str) -> datetime: + return datetime.strptime(value, format).replace(tzinfo=timezone.utc) + + +def is_last_seen_equal_to_first_seen(first_seen: datetime, last_seen: datetime) -> bool: + if last_seen == first_seen: + return True + return False diff --git a/internal-enrichment/kaspersky-enrichment/src/kaspersky_client/api_client.py b/internal-enrichment/kaspersky-enrichment/src/kaspersky_client/api_client.py index ed6850db35..36ba28f387 100644 --- a/internal-enrichment/kaspersky-enrichment/src/kaspersky_client/api_client.py +++ b/internal-enrichment/kaspersky-enrichment/src/kaspersky_client/api_client.py @@ -69,10 +69,20 @@ def _request_data(self, api_url: str, params: dict) -> requests.Response: self.helper.connector_logger.error("Unknown error", {"error": err}) raise - def get_file_info(self, obs_hash: str) -> dict: + def get_file_info(self, obs_hash: str, sections: str) -> dict: """ Retrieve file information """ file_url = f"{self.base_url}api/hash/{obs_hash}" + self.params["sections"] = sections + response = self._request_data(file_url, params=self.params) + return response.json() + + def get_ipv4_info(self, obs_ipv4: str, sections: str) -> dict: + """ + Retrieve ipv4 information + """ + file_url = f"{self.base_url}api/ip/{obs_ipv4}" + self.params["sections"] = sections response = self._request_data(file_url, params=self.params) return response.json()