From da673bae9e648b0ac3073c6300580d6bed40ba3e Mon Sep 17 00:00:00 2001 From: xsoar-bot Date: Mon, 9 Jun 2025 19:19:39 +0000 Subject: [PATCH 01/12] "pack contribution initial commit" --- Packs/SOCRadarTakedown/.pack-ignore | 0 Packs/SOCRadarTakedown/.secrets-ignore | 0 .../Integrations/SOCRadarTakedown/README.md | 160 ++++ .../SOCRadarTakedown/SOCRadarTakedown.py | 681 ++++++++++++++++++ .../SOCRadarTakedown/SOCRadarTakedown.yml | 233 ++++++ .../SOCRadarTakedown_image.png | 6 + Packs/SOCRadarTakedown/README.md | 0 Packs/SOCRadarTakedown/pack_metadata.json | 21 + 8 files changed, 1101 insertions(+) create mode 100644 Packs/SOCRadarTakedown/.pack-ignore create mode 100644 Packs/SOCRadarTakedown/.secrets-ignore create mode 100644 Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/README.md create mode 100644 Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py create mode 100644 Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml create mode 100644 Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_image.png create mode 100644 Packs/SOCRadarTakedown/README.md create mode 100644 Packs/SOCRadarTakedown/pack_metadata.json diff --git a/Packs/SOCRadarTakedown/.pack-ignore b/Packs/SOCRadarTakedown/.pack-ignore new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Packs/SOCRadarTakedown/.secrets-ignore b/Packs/SOCRadarTakedown/.secrets-ignore new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/README.md b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/README.md new file mode 100644 index 000000000000..d2d9dd3f80e4 --- /dev/null +++ b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/README.md @@ -0,0 +1,160 @@ +Submit and monitor takedown requests for phishing domains, impersonating accounts, and other digital risks +## Configure SOCRadar Takedown in Cortex + + +| **Parameter** | **Required** | +| --- | --- | +| API Key | True | +| Company ID | True | +| Trust any certificate (not secure) | False | +| Use system proxy settings | False | +| Reliability | False | + +## Commands + +You can execute these commands from the CLI, as part of an automation, or in a playbook. +After you successfully execute a command, a DBot message appears in the War Room with the command details. + +### socradar-submit-phishing-domain + +*** +Submits a takedown request for a phishing or malicious domain + +#### Base Command + +`socradar-submit-phishing-domain` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| domain | The phishing domain to submit for takedown. | Required | +| abuse_type | Type of abuse (default is potential_phishing). Possible values are: potential_phishing, malware, fake_site. | Optional | +| type | Type of domain (default is phishing_domain). Possible values are: phishing_domain, malicious_domain. | Optional | +| notes | Additional information about the takedown request. | Optional | +| send_alarm | Whether to send an alarm (default is true). Possible values are: true, false. | Optional | +| email | Email to receive notifications about the takedown request. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SOCRadarTakedown.PhishingDomain.Domain | string | The domain submitted for takedown | +| SOCRadarTakedown.PhishingDomain.AbuseType | string | Type of abuse | +| SOCRadarTakedown.PhishingDomain.Status | string | Status of the takedown request | +| SOCRadarTakedown.PhishingDomain.Message | string | Message returned from the API | +| SOCRadarTakedown.PhishingDomain.SendAlarm | boolean | Whether an alarm was sent | +| SOCRadarTakedown.PhishingDomain.Notes | string | Notes provided with the takedown request | + +### socradar-submit-social-media-impersonation + +*** +Submits a takedown request for an impersonating social media account + +#### Base Command + +`socradar-submit-social-media-impersonation` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| username | Username of the impersonating account. | Required | +| full_name | Full name shown on the impersonating account. | Required | +| account_type | Type of social media platform. Possible values are: facebook, twitter, instagram, linkedin, tiktok, youtube, other. | Required | +| description | Description or ID of the impersonation case. | Optional | +| followers | Number of followers (default is 0). | Optional | +| profile_picture | URL to the profile picture. | Optional | +| notes | Additional information about the takedown request. | Optional | +| send_alarm | Whether to send an alarm (default is false). Possible values are: true, false. | Optional | +| email | Email to receive notifications about the takedown request. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SOCRadarTakedown.SocialMediaImpersonation.Username | string | Username of the impersonating account | +| SOCRadarTakedown.SocialMediaImpersonation.FullName | string | Full name shown on the impersonating account | +| SOCRadarTakedown.SocialMediaImpersonation.AccountType | string | Type of social media platform | +| SOCRadarTakedown.SocialMediaImpersonation.Status | string | Status of the takedown request | +| SOCRadarTakedown.SocialMediaImpersonation.Message | string | Message returned from the API | +| SOCRadarTakedown.SocialMediaImpersonation.SendAlarm | boolean | Whether an alarm was sent | +| SOCRadarTakedown.SocialMediaImpersonation.Notes | string | Notes provided with the takedown request | + +### socradar-get-takedown-progress + +*** +Gets the progress of a takedown request + +#### Base Command + +`socradar-get-takedown-progress` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| asset_id | The ID of the asset for which to check progress. | Required | +| type | Type of takedown request. Possible values are: phishing_domain, impersonating_accounts, source_code_leaks, rogue_mobile_apps. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SOCRadarTakedown.Progress.AssetID | string | The ID of the asset | +| SOCRadarTakedown.Progress.Type | string | Type of takedown request | +| SOCRadarTakedown.Progress.Status | string | Status of the API request | +| SOCRadarTakedown.Progress.Data | unknown | Progress data returned from the API | +| SOCRadarTakedown.Progress.Message | string | Message returned from the API | + +### socradar-submit-source-code-leak + +*** +Submits a takedown request for leaked source code + +#### Base Command + +`socradar-submit-source-code-leak` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| id | ID of the source code leak to takedown. | Required | +| notes | Additional information about the takedown request. | Optional | +| email | Email to receive notifications about the takedown request. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SOCRadarTakedown.SourceCodeLeak.LeakID | number | ID of the source code leak | +| SOCRadarTakedown.SourceCodeLeak.Status | string | Status of the takedown request | +| SOCRadarTakedown.SourceCodeLeak.Message | string | Message returned from the API | +| SOCRadarTakedown.SourceCodeLeak.Notes | string | Notes provided with the takedown request | +| SOCRadarTakedown.SourceCodeLeak.Email | string | Email provided for notifications | + +### socradar-submit-rogue-app + +*** +Submits a takedown request for a rogue mobile app + +#### Base Command + +`socradar-submit-rogue-app` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| id | ID of the rogue mobile app to takedown. | Required | +| email | Email to receive notifications about the takedown request. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| SOCRadarTakedown.RogueApp.AppID | string | ID of the rogue mobile app | +| SOCRadarTakedown.RogueApp.Status | string | Status of the takedown request | +| SOCRadarTakedown.RogueApp.Message | string | Message returned from the API | +| SOCRadarTakedown.RogueApp.Email | string | Email provided for notifications | diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py new file mode 100644 index 000000000000..bd63d1406987 --- /dev/null +++ b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py @@ -0,0 +1,681 @@ +import demistomock as demisto # noqa: F401 +from CommonServerPython import * # noqa: F401 +import urllib3 +import traceback +import re +from typing import Any, Dict, List, Optional, Union +from json.decoder import JSONDecodeError + +# Disable insecure warnings +urllib3.disable_warnings() + +""" CONSTANTS """ + +SOCRADAR_API_ENDPOINT = "https://platform.socradar.com/api" +MESSAGES = { + "BAD_REQUEST_ERROR": "An error occurred while fetching the data.", + "AUTHORIZATION_ERROR": "Authorization Error: make sure API Key is correctly set.", + "RATE_LIMIT_EXCEED_ERROR": "Rate limit has been exceeded. Please make sure your API key's rate limit is adequate.", +} + +""" CLIENT CLASS """ + + +class Client: + """ + Client class to interact with the SOCRadar Takedown API + """ + + def __init__(self, base_url, api_key, company_id, verify, proxy): + self.base_url = base_url + self.api_key = api_key + self.company_id = company_id + self.headers = {"API-KEY": self.api_key} + self.verify = verify + self.proxy = proxy + + def check_auth(self): + """Checks if the API key is valid""" + import requests + + url = f"{self.base_url}/get/company/{self.company_id}/takedown/progress" + params = {"asset_id": "test", "type": "impersonating_accounts"} + + try: + response = requests.get( + url, + params=params, + headers=self.headers, + verify=self.verify + ) + + if response.status_code == 401: + raise Exception("Authorization Error: Invalid API Key") + elif response.status_code == 429: + raise Exception("Rate limit exceeded") + elif response.status_code >= 400: + raise Exception(f"API Error: {response.status_code}") + + return {"is_success": True} + + except requests.exceptions.RequestException as e: + raise Exception(f"Connection error: {str(e)}") + + def submit_phishing_domain_takedown(self, domain, abuse_type="potential_phishing", notes="", + domain_type="phishing_domain", send_alarm=True, email=""): + """Submits a takedown request for a phishing domain""" + import requests + + url = f"{self.base_url}/add/company/{self.company_id}/takedown/request" + data = { + "abuse_type": abuse_type, + "entity": domain, + "type": domain_type, + "notes": notes, + "send_alarm": send_alarm, + "email": email + } + + response = requests.post( + url, + json=data, + headers=self.headers, + verify=self.verify + ) + + if response.status_code >= 400: + raise Exception(f"API Error: {response.status_code} - {response.text}") + + return response.json() + + def submit_social_media_impersonation_takedown(self, url_link, abuse_type="impersonating_accounts", + notes="", send_alarm=True, email=""): + """Submits a takedown request for social media impersonation""" + import requests + + api_url = f"{self.base_url}/add/company/{self.company_id}/takedown/request" + data = { + "abuse_type": abuse_type, + "entity": url_link, + "type": "impersonating_accounts", + "notes": notes, + "send_alarm": send_alarm, + "email": email + } + + response = requests.post( + api_url, + json=data, + headers=self.headers, + verify=self.verify + ) + + if response.status_code >= 400: + raise Exception(f"API Error: {response.status_code} - {response.text}") + + return response.json() + + def get_takedown_progress(self, asset_id, takedown_type): + """Gets the progress of a takedown request""" + import requests + + url = f"{self.base_url}/get/company/{self.company_id}/takedown/progress" + params = { + "asset_id": asset_id, + "type": takedown_type + } + + try: + response = requests.get( + url, + params=params, + headers=self.headers, + verify=self.verify + ) + + if response.status_code == 401: + raise Exception("Authorization Error: Invalid API Key") + elif response.status_code == 429: + raise Exception("Rate limit exceeded") + elif response.status_code >= 400: + raise Exception(f"API Error: {response.status_code} - {response.text}") + + return response.json() + + except requests.exceptions.RequestException as e: + raise Exception(f"Connection error: {str(e)}") + + def submit_source_code_leak_takedown(self, url_link, abuse_type="source_code_leak", + notes="", send_alarm=True, email=""): + """Submits a takedown request for leaked source code""" + import requests + + api_url = f"{self.base_url}/add/company/{self.company_id}/takedown/request" + data = { + "abuse_type": abuse_type, + "entity": url_link, + "type": "source_code_leak", + "notes": notes, + "send_alarm": send_alarm, + "email": email + } + + response = requests.post( + api_url, + json=data, + headers=self.headers, + verify=self.verify + ) + + if response.status_code >= 400: + raise Exception(f"API Error: {response.status_code} - {response.text}") + + return response.json() + + def submit_rogue_app_takedown(self, app_info, abuse_type="rogue_mobile_app", + notes="", send_alarm=True, email=""): + """Submits a takedown request for a rogue mobile app""" + import requests + + api_url = f"{self.base_url}/add/company/{self.company_id}/takedown/request" + data = { + "abuse_type": abuse_type, + "entity": app_info, + "type": "rogue_mobile_app", + "notes": notes, + "send_alarm": send_alarm, + "email": email + } + + response = requests.post( + api_url, + json=data, + headers=self.headers, + verify=self.verify + ) + + if response.status_code >= 400: + raise Exception(f"API Error: {response.status_code} - {response.text}") + + return response.json() + + +""" HELPER FUNCTIONS """ + + +class Validator: + @staticmethod + def validate_domain(domain_to_validate): + if not isinstance(domain_to_validate, str) or len(domain_to_validate) > 255: + return False + if domain_to_validate.endswith("."): + domain_to_validate = domain_to_validate[:-1] + domain_regex = re.compile(r"(?!-)[A-Z\d-]{1,63}(? + + + + + diff --git a/Packs/SOCRadarTakedown/README.md b/Packs/SOCRadarTakedown/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/Packs/SOCRadarTakedown/pack_metadata.json b/Packs/SOCRadarTakedown/pack_metadata.json new file mode 100644 index 000000000000..acaa6dc36876 --- /dev/null +++ b/Packs/SOCRadarTakedown/pack_metadata.json @@ -0,0 +1,21 @@ +{ + "name": "SOCRadarTakedown", + "description": "", + "support": "community", + "currentVersion": "1.0.0", + "author": "Radargoger", + "url": "", + "email": "", + "created": "2025-06-09T19:19:21Z", + "categories": [], + "tags": [], + "useCases": [], + "keywords": [], + "marketplaces": [ + "xsoar", + "marketplacev2" + ], + "githubUser": [ + "Radargoger" + ] +} \ No newline at end of file From 026625587eebd12eb4a45d1d1fa028d537b22515 Mon Sep 17 00:00:00 2001 From: Radargoger Date: Wed, 25 Jun 2025 12:29:22 +0300 Subject: [PATCH 02/12] Update pack_metadata.json update pack_metadajson --- Packs/SOCRadarTakedown/pack_metadata.json | 35 ++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/Packs/SOCRadarTakedown/pack_metadata.json b/Packs/SOCRadarTakedown/pack_metadata.json index acaa6dc36876..91356622e0dd 100644 --- a/Packs/SOCRadarTakedown/pack_metadata.json +++ b/Packs/SOCRadarTakedown/pack_metadata.json @@ -1,21 +1,30 @@ { "name": "SOCRadarTakedown", - "description": "", + "description": "Submit and manage takedown requests for phishing domains, social media impersonation, source code leaks, and rogue mobile apps through the SOCRadar platform. This pack helps security teams automate the process of reporting and tracking malicious content for removal.", "support": "community", "currentVersion": "1.0.0", - "author": "Radargoger", + "author": "Community Contributor", "url": "", "email": "", - "created": "2025-06-09T19:19:21Z", - "categories": [], - "tags": [], - "useCases": [], - "keywords": [], - "marketplaces": [ - "xsoar", - "marketplacev2" + "created": "2024-06-24T00:00:00Z", + "categories": [ + "Threat Intelligence" ], - "githubUser": [ - "Radargoger" + "tags": [ + "takedown", + "phishing", + "threat-intelligence", + "brand-protection" + ], + "useCases": [ + "Brand Protection", + "Threat Intelligence Management" + ], + "keywords": [ + "socradar", + "takedown", + "phishing", + "impersonation", + "brand protection" ] -} \ No newline at end of file +} From affd7e15e8090760853a0cdc965a7090c3d9f7aa Mon Sep 17 00:00:00 2001 From: Radargoger Date: Wed, 25 Jun 2025 12:29:55 +0300 Subject: [PATCH 03/12] Update SOCRadarTakedown.yml --- .../SOCRadarTakedown/SOCRadarTakedown.yml | 354 +++++++++--------- 1 file changed, 178 insertions(+), 176 deletions(-) diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml index bd4048a0f012..1c0503a1388a 100644 --- a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml +++ b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml @@ -1,233 +1,235 @@ -category: Data Enrichment & Threat Intelligence commonfields: - id: SOCRadarTakedown + id: SOCRadar Takedown version: -1 +name: SOCRadar Takedown +display: SOCRadar Takedown +category: Threat Intelligence +description: Submit and track takedown requests for phishing domains, social media impersonation, source code leaks, and rogue mobile apps through SOCRadar platform. +detaileddescription: | + This integration allows you to: + - Submit takedown requests for phishing domains + - Submit takedown requests for social media impersonation + - Submit takedown requests for source code leaks + - Submit takedown requests for rogue mobile apps + - Track the progress of submitted takedown requests + + ## Authentication + You need a valid SOCRadar API key and Company ID to use this integration. + + ## Rate Limits + Please ensure your API key has adequate rate limits for your usage. + configuration: -- display: API Key +- display: SOCRadar API Key name: apikey required: true type: 4 + additionalinfo: Your SOCRadar platform API key - display: Company ID name: company_id required: true type: 0 + additionalinfo: Your SOCRadar Company ID - display: Trust any certificate (not secure) name: insecure required: false type: 8 + defaultvalue: false - display: Use system proxy settings name: proxy required: false type: 8 -- defaultvalue: B - Usually reliable - display: Reliability - name: integrationReliability - options: - - A+ - 3rd party enrichment - - A - Completely reliable - - B - Usually reliable - - C - Fairly reliable - - D - Not usually reliable - - E - Unreliable - - F - Reliability cannot be judged - required: false - type: 15 -description: Submit and monitor takedown requests for phishing domains, impersonating accounts, and other digital risks. -display: SOCRadar Takedown -name: SOCRadarTakedown + defaultvalue: false + script: + script: '' + type: python + subtype: python3 + dockerimage: demisto/python3:3.10.13.72123 commands: - - arguments: - - description: The phishing domain to submit for takedown. - name: domain + - name: socradar-submit-phishing-domain + description: Submit a takedown request for a phishing domain + arguments: + - name: domain + description: The phishing domain to be taken down required: true - - auto: PREDEFINED - description: Type of abuse (default is potential_phishing). - name: abuse_type + - name: abuse_type + description: Type of abuse + defaultValue: potential_phishing predefined: - potential_phishing - - malware - - fake_site - - auto: PREDEFINED - description: Type of domain (default is phishing_domain). - name: type + - confirmed_phishing + - name: type + description: Domain type + defaultValue: phishing_domain predefined: - phishing_domain - - malicious_domain - - description: Additional information about the takedown request. - name: notes - - auto: PREDEFINED - description: Whether to send an alarm (default is true). - name: send_alarm + - lookalike_domain + - name: notes + description: Additional notes for the takedown request + required: false + - name: send_alarm + description: Whether to send alarm notification + defaultValue: "true" predefined: - - 'true' - - 'false' - - description: Email to receive notifications about the takedown request. - name: email + - "true" + - "false" + - name: email + description: Email address for notifications required: true - description: Submits a takedown request for a phishing or malicious domain. - name: socradar-submit-phishing-domain outputs: - contextPath: SOCRadarTakedown.PhishingDomain.Domain - description: The domain submitted for takedown. - type: string + description: The domain that was reported + type: String - contextPath: SOCRadarTakedown.PhishingDomain.AbuseType - description: Type of abuse. - type: string + description: Type of abuse reported + type: String - contextPath: SOCRadarTakedown.PhishingDomain.Status - description: Status of the takedown request. - type: string + description: Status of the takedown request + type: String - contextPath: SOCRadarTakedown.PhishingDomain.Message - description: Message returned from the API. - type: string + description: Response message from the API + type: String - contextPath: SOCRadarTakedown.PhishingDomain.SendAlarm - description: Whether an alarm was sent. - type: boolean + description: Whether alarm notification is enabled + type: Boolean - contextPath: SOCRadarTakedown.PhishingDomain.Notes - description: Notes provided with the takedown request. - type: string - - arguments: - - description: Username of the impersonating account. - name: username + description: Additional notes for the request + type: String + - name: socradar-submit-social-media-impersonation + description: Submit a takedown request for social media impersonation + arguments: + - name: url + description: URL of the impersonating social media account required: true - - description: Full name shown on the impersonating account. - name: full_name - required: true - - auto: PREDEFINED - description: Type of social media platform. - name: account_type + - name: abuse_type + description: Type of abuse + defaultValue: impersonating_accounts predefined: - - facebook - - twitter - - instagram - - linkedin - - tiktok - - youtube - - other - required: true - - description: Description or ID of the impersonation case. - name: description - - description: Number of followers (default is 0). - name: followers - - description: URL to the profile picture. - name: profile_picture - - description: Additional information about the takedown request. - name: notes - - auto: PREDEFINED - description: Whether to send an alarm (default is false). - name: send_alarm + - impersonating_accounts + - fake_profiles + - name: notes + description: Additional notes for the takedown request + required: false + - name: send_alarm + description: Whether to send alarm notification + defaultValue: "true" predefined: - - 'true' - - 'false' - - description: Email to receive notifications about the takedown request. - name: email - description: Submits a takedown request for an impersonating social media account. - name: socradar-submit-social-media-impersonation + - "true" + - "false" + - name: email + description: Email address for notifications + required: true outputs: - - contextPath: SOCRadarTakedown.SocialMediaImpersonation.Username - description: Username of the impersonating account. - type: string - - contextPath: SOCRadarTakedown.SocialMediaImpersonation.FullName - description: Full name shown on the impersonating account. - type: string - - contextPath: SOCRadarTakedown.SocialMediaImpersonation.AccountType - description: Type of social media platform. - type: string + - contextPath: SOCRadarTakedown.SocialMediaImpersonation.URL + description: The URL that was reported + type: String + - contextPath: SOCRadarTakedown.SocialMediaImpersonation.AbuseType + description: Type of abuse reported + type: String - contextPath: SOCRadarTakedown.SocialMediaImpersonation.Status - description: Status of the takedown request. - type: string + description: Status of the takedown request + type: String - contextPath: SOCRadarTakedown.SocialMediaImpersonation.Message - description: Message returned from the API. - type: string + description: Response message from the API + type: String - contextPath: SOCRadarTakedown.SocialMediaImpersonation.SendAlarm - description: Whether an alarm was sent. - type: boolean + description: Whether alarm notification is enabled + type: Boolean - contextPath: SOCRadarTakedown.SocialMediaImpersonation.Notes - description: Notes provided with the takedown request. - type: string - - arguments: - - description: The ID of the asset for which to check progress. - name: asset_id + description: Additional notes for the request + type: String + - name: socradar-submit-source-code-leak + description: Submit a takedown request for leaked source code + arguments: + - name: url + description: URL where the source code leak is found required: true - - auto: PREDEFINED - description: Type of takedown request. - name: type + - name: abuse_type + description: Type of abuse + defaultValue: source_code_leak predefined: - - phishing_domain - - impersonating_accounts - - source_code_leaks - - rogue_mobile_apps - required: true - description: Gets the progress of a takedown request. - name: socradar-get-takedown-progress - outputs: - - contextPath: SOCRadarTakedown.Progress.AssetID - description: The ID of the asset. - type: string - - contextPath: SOCRadarTakedown.Progress.Type - description: Type of takedown request. - type: string - - contextPath: SOCRadarTakedown.Progress.Status - description: Status of the API request. - type: string - - contextPath: SOCRadarTakedown.Progress.Data - description: Progress data returned from the API. - type: unknown - - contextPath: SOCRadarTakedown.Progress.Message - description: Message returned from the API. - type: string - - arguments: - - description: ID of the source code leak to takedown. - name: id + - source_code_leak + - data_leak + - name: notes + description: Additional notes for the takedown request + required: false + - name: send_alarm + description: Whether to send alarm notification + defaultValue: "true" + predefined: + - "true" + - "false" + - name: email + description: Email address for notifications required: true - - description: Additional information about the takedown request. - name: notes - - description: Email to receive notifications about the takedown request. - name: email - description: Submits a takedown request for leaked source code. - name: socradar-submit-source-code-leak outputs: - - contextPath: SOCRadarTakedown.SourceCodeLeak.LeakID - description: ID of the source code leak. - type: number + - contextPath: SOCRadarTakedown.SourceCodeLeak.URL + description: The URL that was reported + type: String + - contextPath: SOCRadarTakedown.SourceCodeLeak.AbuseType + description: Type of abuse reported + type: String - contextPath: SOCRadarTakedown.SourceCodeLeak.Status - description: Status of the takedown request. - type: string + description: Status of the takedown request + type: String - contextPath: SOCRadarTakedown.SourceCodeLeak.Message - description: Message returned from the API. - type: string + description: Response message from the API + type: String + - contextPath: SOCRadarTakedown.SourceCodeLeak.SendAlarm + description: Whether alarm notification is enabled + type: Boolean - contextPath: SOCRadarTakedown.SourceCodeLeak.Notes - description: Notes provided with the takedown request. - type: string - - contextPath: SOCRadarTakedown.SourceCodeLeak.Email - description: Email provided for notifications. - type: string - - arguments: - - description: ID of the rogue mobile app to takedown. - name: id + description: Additional notes for the request + type: String + - name: socradar-submit-rogue-app + description: Submit a takedown request for a rogue mobile app + arguments: + - name: app_info + description: Information about the rogue mobile app (name, store URL, etc.) + required: true + - name: abuse_type + description: Type of abuse + defaultValue: rogue_mobile_app + predefined: + - rogue_mobile_app + - malicious_app + - name: notes + description: Additional notes for the takedown request + required: false + - name: send_alarm + description: Whether to send alarm notification + defaultValue: "true" + predefined: + - "true" + - "false" + - name: email + description: Email address for notifications required: true - - description: Email to receive notifications about the takedown request. - name: email - description: Submits a takedown request for a rogue mobile app. - name: socradar-submit-rogue-app outputs: - - contextPath: SOCRadarTakedown.RogueApp.AppID - description: ID of the rogue mobile app. - type: string + - contextPath: SOCRadarTakedown.RogueApp.AppInfo + description: Information about the app that was reported + type: String + - contextPath: SOCRadarTakedown.RogueApp.AbuseType + description: Type of abuse reported + type: String - contextPath: SOCRadarTakedown.RogueApp.Status - description: Status of the takedown request. - type: string + description: Status of the takedown request + type: String - contextPath: SOCRadarTakedown.RogueApp.Message - description: Message returned from the API. - type: string - - contextPath: SOCRadarTakedown.RogueApp.Email - description: Email provided for notifications. - type: string - dockerimage: demisto/python3:3.12.8.3296088 + description: Response message from the API + type: String + - contextPath: SOCRadarTakedown.RogueApp.SendAlarm + description: Whether alarm notification is enabled + type: Boolean + - contextPath: SOCRadarTakedown.RogueApp.Notes + description: Additional notes for the request + type: String runonce: false - script: '' - subtype: python3 - type: python + ismappable: false + isremotesyncin: false + isremotesyncout: false + fromversion: 6.0.0 tests: - No tests (auto formatted) From bfd3a62bd84687fc28e25ce3f8d3b322ba68cc9c Mon Sep 17 00:00:00 2001 From: Radargoger Date: Wed, 25 Jun 2025 12:30:16 +0300 Subject: [PATCH 04/12] Update SOCRadarTakedown.py --- .../SOCRadarTakedown/SOCRadarTakedown.py | 781 ++++++------------ 1 file changed, 234 insertions(+), 547 deletions(-) diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py index bd63d1406987..e5c02a8a1556 100644 --- a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py +++ b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py @@ -1,11 +1,12 @@ -import demistomock as demisto # noqa: F401 -from CommonServerPython import * # noqa: F401 import urllib3 import traceback import re from typing import Any, Dict, List, Optional, Union from json.decoder import JSONDecodeError +# Import XSOAR common functions +from CommonServerPython import * + # Disable insecure warnings urllib3.disable_warnings() @@ -20,13 +21,12 @@ """ CLIENT CLASS """ - class Client: """ Client class to interact with the SOCRadar Takedown API """ - def __init__(self, base_url, api_key, company_id, verify, proxy): + def __init__(self, base_url: str, api_key: str, company_id: str, verify: bool, proxy: bool): self.base_url = base_url self.api_key = api_key self.company_id = company_id @@ -34,17 +34,13 @@ def __init__(self, base_url, api_key, company_id, verify, proxy): self.verify = verify self.proxy = proxy - def check_auth(self): + def check_auth(self) -> Dict[str, Any]: """Checks if the API key is valid""" - import requests - - url = f"{self.base_url}/get/company/{self.company_id}/takedown/progress" - params = {"asset_id": "test", "type": "impersonating_accounts"} - + url = f"{self.base_url}/get/company/{self.company_id}/takedown/requests" + try: response = requests.get( url, - params=params, headers=self.headers, verify=self.verify ) @@ -53,6 +49,8 @@ def check_auth(self): raise Exception("Authorization Error: Invalid API Key") elif response.status_code == 429: raise Exception("Rate limit exceeded") + elif response.status_code >= 500: + raise Exception(f"Server Error: {response.status_code}") elif response.status_code >= 400: raise Exception(f"API Error: {response.status_code}") @@ -61,16 +59,14 @@ def check_auth(self): except requests.exceptions.RequestException as e: raise Exception(f"Connection error: {str(e)}") - def submit_phishing_domain_takedown(self, domain, abuse_type="potential_phishing", notes="", - domain_type="phishing_domain", send_alarm=True, email=""): - """Submits a takedown request for a phishing domain""" - import requests - + def submit_takedown_request(self, entity: str, request_type: str, abuse_type: str, + notes: str = "", send_alarm: bool = True, email: str = "") -> Dict[str, Any]: + """Generic method to submit takedown requests""" url = f"{self.base_url}/add/company/{self.company_id}/takedown/request" data = { "abuse_type": abuse_type, - "entity": domain, - "type": domain_type, + "entity": entity, + "type": request_type, "notes": notes, "send_alarm": send_alarm, "email": email @@ -88,124 +84,11 @@ def submit_phishing_domain_takedown(self, domain, abuse_type="potential_phishing return response.json() - def submit_social_media_impersonation_takedown(self, url_link, abuse_type="impersonating_accounts", - notes="", send_alarm=True, email=""): - """Submits a takedown request for social media impersonation""" - import requests - - api_url = f"{self.base_url}/add/company/{self.company_id}/takedown/request" - data = { - "abuse_type": abuse_type, - "entity": url_link, - "type": "impersonating_accounts", - "notes": notes, - "send_alarm": send_alarm, - "email": email - } - - response = requests.post( - api_url, - json=data, - headers=self.headers, - verify=self.verify - ) - - if response.status_code >= 400: - raise Exception(f"API Error: {response.status_code} - {response.text}") - - return response.json() - - def get_takedown_progress(self, asset_id, takedown_type): - """Gets the progress of a takedown request""" - import requests - - url = f"{self.base_url}/get/company/{self.company_id}/takedown/progress" - params = { - "asset_id": asset_id, - "type": takedown_type - } - - try: - response = requests.get( - url, - params=params, - headers=self.headers, - verify=self.verify - ) - - if response.status_code == 401: - raise Exception("Authorization Error: Invalid API Key") - elif response.status_code == 429: - raise Exception("Rate limit exceeded") - elif response.status_code >= 400: - raise Exception(f"API Error: {response.status_code} - {response.text}") - - return response.json() - - except requests.exceptions.RequestException as e: - raise Exception(f"Connection error: {str(e)}") - - def submit_source_code_leak_takedown(self, url_link, abuse_type="source_code_leak", - notes="", send_alarm=True, email=""): - """Submits a takedown request for leaked source code""" - import requests - - api_url = f"{self.base_url}/add/company/{self.company_id}/takedown/request" - data = { - "abuse_type": abuse_type, - "entity": url_link, - "type": "source_code_leak", - "notes": notes, - "send_alarm": send_alarm, - "email": email - } - - response = requests.post( - api_url, - json=data, - headers=self.headers, - verify=self.verify - ) - - if response.status_code >= 400: - raise Exception(f"API Error: {response.status_code} - {response.text}") - - return response.json() - - def submit_rogue_app_takedown(self, app_info, abuse_type="rogue_mobile_app", - notes="", send_alarm=True, email=""): - """Submits a takedown request for a rogue mobile app""" - import requests - - api_url = f"{self.base_url}/add/company/{self.company_id}/takedown/request" - data = { - "abuse_type": abuse_type, - "entity": app_info, - "type": "rogue_mobile_app", - "notes": notes, - "send_alarm": send_alarm, - "email": email - } - - response = requests.post( - api_url, - json=data, - headers=self.headers, - verify=self.verify - ) - - if response.status_code >= 400: - raise Exception(f"API Error: {response.status_code} - {response.text}") - - return response.json() - - """ HELPER FUNCTIONS """ - class Validator: @staticmethod - def validate_domain(domain_to_validate): + def validate_domain(domain_to_validate: str) -> bool: if not isinstance(domain_to_validate, str) or len(domain_to_validate) > 255: return False if domain_to_validate.endswith("."): @@ -219,7 +102,7 @@ def raise_if_domain_not_valid(domain: str): raise ValueError(f'Domain "{domain}" is not a valid domain address') @staticmethod - def validate_url(url: str): + def validate_url(url: str) -> bool: """Basic URL validation""" url_pattern = re.compile( r'^https?://' # http:// or https:// @@ -235,445 +118,249 @@ def raise_if_url_not_valid(url: str): if not Validator.validate_url(url): raise ValueError(f'URL "{url}" is not a valid URL') +def get_client_from_params() -> Client: + """Initialize client from demisto params""" + api_key = demisto.params().get("apikey", "").strip() + company_id = demisto.params().get("company_id", "").strip() + verify_certificate = not demisto.params().get("insecure", False) + proxy = demisto.params().get("proxy", False) + + if not api_key: + raise ValueError("API Key is required") + if not company_id: + raise ValueError("Company ID is required") + + return Client( + base_url=SOCRADAR_API_ENDPOINT, + api_key=api_key, + company_id=company_id, + verify=verify_certificate, + proxy=proxy + ) """ COMMAND FUNCTIONS """ - -def test_module(): +def test_module(client: Client) -> str: """Tests API connectivity and authentication""" try: - # Get parameters - api_key = demisto.params().get("apikey", "") - company_id = demisto.params().get("company_id", "") - verify_certificate = not demisto.params().get("insecure", False) - proxy = demisto.params().get("proxy", False) - - if not api_key: - return "API Key is required" - if not company_id: - return "Company ID is required" - - # Create client and test - client = Client( - base_url=SOCRADAR_API_ENDPOINT, - api_key=api_key, - company_id=company_id, - verify=verify_certificate, - proxy=proxy - ) - client.check_auth() return "ok" - except Exception as e: return f"Test failed: {str(e)}" - -def submit_phishing_domain_takedown_command(): +def submit_phishing_domain_takedown_command(client: Client) -> CommandResults: """Submits a takedown request for a phishing domain""" - try: - # Get parameters - api_key = demisto.params().get("apikey", "") - company_id = demisto.params().get("company_id", "") - verify_certificate = not demisto.params().get("insecure", False) - proxy = demisto.params().get("proxy", False) - - # Get arguments - domain = demisto.args().get("domain", "") - abuse_type = demisto.args().get("abuse_type", "potential_phishing") - domain_type = demisto.args().get("type", "phishing_domain") - notes = demisto.args().get("notes", "") - send_alarm = demisto.args().get("send_alarm", "true").lower() == "true" - email = demisto.args().get("email", "") - - # Validate required fields - if not domain: - raise ValueError("Domain is required") - if not email: - raise ValueError("Email is required") - - # Validate domain - Validator.raise_if_domain_not_valid(domain) - - # Create client - client = Client( - base_url=SOCRADAR_API_ENDPOINT, - api_key=api_key, - company_id=company_id, - verify=verify_certificate, - proxy=proxy - ) - - # Submit request - raw_response = client.submit_phishing_domain_takedown( - domain, abuse_type, notes, domain_type, send_alarm, email - ) - - # Prepare output - readable_output = f"### Phishing Domain Takedown Request\n" - readable_output += f"**Domain**: {domain}\n" - readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" - - if raw_response.get("message"): - readable_output += f"**Message**: {raw_response.get('message')}\n" - - outputs = { - "SOCRadarTakedown.PhishingDomain(val.Domain == obj.Domain)": { - "Domain": domain, - "AbuseType": abuse_type, - "Status": "Success" if raw_response.get('is_success', False) else "Failed", - "Message": raw_response.get("message", ""), - "SendAlarm": send_alarm, - "Notes": notes - } - } - - demisto.results({ - "Type": entryTypes["note"], - "Contents": raw_response, - "ContentsFormat": formats["json"], - "HumanReadable": readable_output, - "EntryContext": outputs - }) - - except Exception as e: - demisto.results({ - "Type": entryTypes["error"], - "Contents": str(e), - "ContentsFormat": formats["text"] - }) - - -def submit_social_media_impersonation_takedown_command(): + args = demisto.args() + domain = args.get("domain", "") + abuse_type = args.get("abuse_type", "potential_phishing") + domain_type = args.get("type", "phishing_domain") + notes = args.get("notes", "") + send_alarm = args.get("send_alarm", "true").lower() == "true" + email = args.get("email", "") + + # Validate domain + Validator.raise_if_domain_not_valid(domain) + + # Submit request + raw_response = client.submit_takedown_request( + entity=domain, + request_type=domain_type, + abuse_type=abuse_type, + notes=notes, + send_alarm=send_alarm, + email=email + ) + + # Prepare output + readable_output = f"### Phishing Domain Takedown Request\n" + readable_output += f"**Domain**: {domain}\n" + readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" + + if raw_response.get("message"): + readable_output += f"**Message**: {raw_response.get('message')}\n" + + outputs = { + "Domain": domain, + "AbuseType": abuse_type, + "Status": "Success" if raw_response.get('is_success', False) else "Failed", + "Message": raw_response.get("message", ""), + "SendAlarm": send_alarm, + "Notes": notes + } + + return CommandResults( + outputs_prefix="SOCRadarTakedown.PhishingDomain", + outputs_key_field="Domain", + outputs=outputs, + readable_output=readable_output, + raw_response=raw_response + ) + +def submit_social_media_impersonation_takedown_command(client: Client) -> CommandResults: """Submits a takedown request for social media impersonation""" - try: - # Get parameters - api_key = demisto.params().get("apikey", "") - company_id = demisto.params().get("company_id", "") - verify_certificate = not demisto.params().get("insecure", False) - proxy = demisto.params().get("proxy", False) - - # Get arguments - url_link = demisto.args().get("url", "") - abuse_type = demisto.args().get("abuse_type", "impersonating_accounts") - notes = demisto.args().get("notes", "") - send_alarm = demisto.args().get("send_alarm", "true").lower() == "true" - email = demisto.args().get("email", "") - - # Validate required fields - if not url_link: - raise ValueError("URL is required") - if not email: - raise ValueError("Email is required") - - # Validate URL - Validator.raise_if_url_not_valid(url_link) - - # Create client - client = Client( - base_url=SOCRADAR_API_ENDPOINT, - api_key=api_key, - company_id=company_id, - verify=verify_certificate, - proxy=proxy - ) - - # Submit request - raw_response = client.submit_social_media_impersonation_takedown( - url_link, abuse_type, notes, send_alarm, email - ) - - # Prepare output - readable_output = f"### Social Media Impersonation Takedown Request\n" - readable_output += f"**URL**: {url_link}\n" - readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" - - if raw_response.get("message"): - readable_output += f"**Message**: {raw_response.get('message')}\n" - - outputs = { - "SOCRadarTakedown.SocialMediaImpersonation(val.URL == obj.URL)": { - "URL": url_link, - "AbuseType": abuse_type, - "Status": "Success" if raw_response.get('is_success', False) else "Failed", - "Message": raw_response.get("message", ""), - "SendAlarm": send_alarm, - "Notes": notes - } - } - - demisto.results({ - "Type": entryTypes["note"], - "Contents": raw_response, - "ContentsFormat": formats["json"], - "HumanReadable": readable_output, - "EntryContext": outputs - }) - - except Exception as e: - demisto.results({ - "Type": entryTypes["error"], - "Contents": str(e), - "ContentsFormat": formats["text"] - }) - - -def get_takedown_progress_command(): - """Gets the progress of a takedown request""" - try: - # Get parameters - api_key = demisto.params().get("apikey", "") - company_id = demisto.params().get("company_id", "") - verify_certificate = not demisto.params().get("insecure", False) - proxy = demisto.params().get("proxy", False) - - # Get arguments - asset_id = demisto.args().get("asset_id", "") - takedown_type = demisto.args().get("type", "") - - # Validate required fields - if not asset_id: - raise ValueError("Asset ID is required") - if not takedown_type: - raise ValueError("Type is required") - - # Create client - client = Client( - base_url=SOCRADAR_API_ENDPOINT, - api_key=api_key, - company_id=company_id, - verify=verify_certificate, - proxy=proxy - ) - - # Get progress - raw_response = client.get_takedown_progress(asset_id, takedown_type) - - # Prepare output - readable_output = f"### Takedown Progress\n" - readable_output += f"**Asset ID**: {asset_id}\n" - readable_output += f"**Type**: {takedown_type}\n" - - if raw_response.get("status"): - readable_output += f"**Status**: {raw_response.get('status')}\n" - if raw_response.get("progress"): - readable_output += f"**Progress**: {raw_response.get('progress')}\n" - if raw_response.get("message"): - readable_output += f"**Message**: {raw_response.get('message')}\n" - - outputs = { - "SOCRadarTakedown.Progress(val.AssetId == obj.AssetId)": { - "AssetId": asset_id, - "Type": takedown_type, - "Status": raw_response.get("status", ""), - "Progress": raw_response.get("progress", ""), - "Message": raw_response.get("message", ""), - "RawResponse": raw_response - } - } - - demisto.results({ - "Type": entryTypes["note"], - "Contents": raw_response, - "ContentsFormat": formats["json"], - "HumanReadable": readable_output, - "EntryContext": outputs - }) - - except Exception as e: - demisto.results({ - "Type": entryTypes["error"], - "Contents": str(e), - "ContentsFormat": formats["text"] - }) - - -def submit_source_code_leak_takedown_command(): + args = demisto.args() + url_link = args.get("url", "") + abuse_type = args.get("abuse_type", "impersonating_accounts") + notes = args.get("notes", "") + send_alarm = args.get("send_alarm", "true").lower() == "true" + email = args.get("email", "") + + # Validate URL + Validator.raise_if_url_not_valid(url_link) + + # Submit request + raw_response = client.submit_takedown_request( + entity=url_link, + request_type="impersonating_accounts", + abuse_type=abuse_type, + notes=notes, + send_alarm=send_alarm, + email=email + ) + + # Prepare output + readable_output = f"### Social Media Impersonation Takedown Request\n" + readable_output += f"**URL**: {url_link}\n" + readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" + + if raw_response.get("message"): + readable_output += f"**Message**: {raw_response.get('message')}\n" + + outputs = { + "URL": url_link, + "AbuseType": abuse_type, + "Status": "Success" if raw_response.get('is_success', False) else "Failed", + "Message": raw_response.get("message", ""), + "SendAlarm": send_alarm, + "Notes": notes + } + + return CommandResults( + outputs_prefix="SOCRadarTakedown.SocialMediaImpersonation", + outputs_key_field="URL", + outputs=outputs, + readable_output=readable_output, + raw_response=raw_response + ) + +def submit_source_code_leak_takedown_command(client: Client) -> CommandResults: """Submits a takedown request for leaked source code""" - try: - # Get parameters - api_key = demisto.params().get("apikey", "") - company_id = demisto.params().get("company_id", "") - verify_certificate = not demisto.params().get("insecure", False) - proxy = demisto.params().get("proxy", False) - - # Get arguments - url_link = demisto.args().get("url", "") - abuse_type = demisto.args().get("abuse_type", "source_code_leak") - notes = demisto.args().get("notes", "") - send_alarm = demisto.args().get("send_alarm", "true").lower() == "true" - email = demisto.args().get("email", "") - - # Validate required fields - if not url_link: - raise ValueError("URL is required") - if not email: - raise ValueError("Email is required") - - # Validate URL - Validator.raise_if_url_not_valid(url_link) - - # Create client - client = Client( - base_url=SOCRADAR_API_ENDPOINT, - api_key=api_key, - company_id=company_id, - verify=verify_certificate, - proxy=proxy - ) - - # Submit request - raw_response = client.submit_source_code_leak_takedown( - url_link, abuse_type, notes, send_alarm, email - ) - - # Prepare output - readable_output = f"### Source Code Leak Takedown Request\n" - readable_output += f"**URL**: {url_link}\n" - readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" - - if raw_response.get("message"): - readable_output += f"**Message**: {raw_response.get('message')}\n" - - outputs = { - "SOCRadarTakedown.SourceCodeLeak(val.URL == obj.URL)": { - "URL": url_link, - "AbuseType": abuse_type, - "Status": "Success" if raw_response.get('is_success', False) else "Failed", - "Message": raw_response.get("message", ""), - "SendAlarm": send_alarm, - "Notes": notes - } - } - - demisto.results({ - "Type": entryTypes["note"], - "Contents": raw_response, - "ContentsFormat": formats["json"], - "HumanReadable": readable_output, - "EntryContext": outputs - }) - - except Exception as e: - demisto.results({ - "Type": entryTypes["error"], - "Contents": str(e), - "ContentsFormat": formats["text"] - }) - - -def submit_rogue_app_takedown_command(): + args = demisto.args() + url_link = args.get("url", "") + abuse_type = args.get("abuse_type", "source_code_leak") + notes = args.get("notes", "") + send_alarm = args.get("send_alarm", "true").lower() == "true" + email = args.get("email", "") + + # Validate URL + Validator.raise_if_url_not_valid(url_link) + + # Submit request + raw_response = client.submit_takedown_request( + entity=url_link, + request_type="source_code_leak", + abuse_type=abuse_type, + notes=notes, + send_alarm=send_alarm, + email=email + ) + + # Prepare output + readable_output = f"### Source Code Leak Takedown Request\n" + readable_output += f"**URL**: {url_link}\n" + readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" + + if raw_response.get("message"): + readable_output += f"**Message**: {raw_response.get('message')}\n" + + outputs = { + "URL": url_link, + "AbuseType": abuse_type, + "Status": "Success" if raw_response.get('is_success', False) else "Failed", + "Message": raw_response.get("message", ""), + "SendAlarm": send_alarm, + "Notes": notes + } + + return CommandResults( + outputs_prefix="SOCRadarTakedown.SourceCodeLeak", + outputs_key_field="URL", + outputs=outputs, + readable_output=readable_output, + raw_response=raw_response + ) + +def submit_rogue_app_takedown_command(client: Client) -> CommandResults: """Submits a takedown request for a rogue mobile app""" - try: - # Get parameters - api_key = demisto.params().get("apikey", "") - company_id = demisto.params().get("company_id", "") - verify_certificate = not demisto.params().get("insecure", False) - proxy = demisto.params().get("proxy", False) - - # Get arguments - app_info = demisto.args().get("app_info", "") - abuse_type = demisto.args().get("abuse_type", "rogue_mobile_app") - notes = demisto.args().get("notes", "") - send_alarm = demisto.args().get("send_alarm", "true").lower() == "true" - email = demisto.args().get("email", "") - - # Validate required fields - if not app_info: - raise ValueError("App info is required") - if not email: - raise ValueError("Email is required") - - # Create client - client = Client( - base_url=SOCRADAR_API_ENDPOINT, - api_key=api_key, - company_id=company_id, - verify=verify_certificate, - proxy=proxy - ) - - # Submit request - raw_response = client.submit_rogue_app_takedown( - app_info, abuse_type, notes, send_alarm, email - ) - - # Prepare output - readable_output = f"### Rogue App Takedown Request\n" - readable_output += f"**App Info**: {app_info}\n" - readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" - - if raw_response.get("message"): - readable_output += f"**Message**: {raw_response.get('message')}\n" - - outputs = { - "SOCRadarTakedown.RogueApp(val.AppInfo == obj.AppInfo)": { - "AppInfo": app_info, - "AbuseType": abuse_type, - "Status": "Success" if raw_response.get('is_success', False) else "Failed", - "Message": raw_response.get("message", ""), - "SendAlarm": send_alarm, - "Notes": notes - } - } - - demisto.results({ - "Type": entryTypes["note"], - "Contents": raw_response, - "ContentsFormat": formats["json"], - "HumanReadable": readable_output, - "EntryContext": outputs - }) - - except Exception as e: - demisto.results({ - "Type": entryTypes["error"], - "Contents": str(e), - "ContentsFormat": formats["text"] - }) - + args = demisto.args() + app_info = args.get("app_info", "") + abuse_type = args.get("abuse_type", "rogue_mobile_app") + notes = args.get("notes", "") + send_alarm = args.get("send_alarm", "true").lower() == "true" + email = args.get("email", "") + + # Submit request + raw_response = client.submit_takedown_request( + entity=app_info, + request_type="rogue_mobile_app", + abuse_type=abuse_type, + notes=notes, + send_alarm=send_alarm, + email=email + ) + + # Prepare output + readable_output = f"### Rogue App Takedown Request\n" + readable_output += f"**App Info**: {app_info}\n" + readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" + + if raw_response.get("message"): + readable_output += f"**Message**: {raw_response.get('message')}\n" + + outputs = { + "AppInfo": app_info, + "AbuseType": abuse_type, + "Status": "Success" if raw_response.get('is_success', False) else "Failed", + "Message": raw_response.get("message", ""), + "SendAlarm": send_alarm, + "Notes": notes + } + + return CommandResults( + outputs_prefix="SOCRadarTakedown.RogueApp", + outputs_key_field="AppInfo", + outputs=outputs, + readable_output=readable_output, + raw_response=raw_response + ) """ MAIN FUNCTION """ - def main(): """Main function, parses params and runs command functions""" try: command = demisto.command() - + if command == "test-module": - result = test_module() - demisto.results(result) - - elif command == "socradar-submit-phishing-domain": - submit_phishing_domain_takedown_command() - - elif command == "socradar-submit-social-media-impersonation": - submit_social_media_impersonation_takedown_command() - - elif command == "socradar-get-takedown-progress": - get_takedown_progress_command() - - elif command == "socradar-submit-source-code-leak": - submit_source_code_leak_takedown_command() - - elif command == "socradar-submit-rogue-app": - submit_rogue_app_takedown_command() - + client = get_client_from_params() + result = test_module(client) + return_results(result) else: - demisto.results({ - "Type": entryTypes["error"], - "Contents": f"Unknown command: {command}", - "ContentsFormat": formats["text"] - }) + client = get_client_from_params() + + if command == "socradar-submit-phishing-domain": + return_results(submit_phishing_domain_takedown_command(client)) + elif command == "socradar-submit-social-media-impersonation": + return_results(submit_social_media_impersonation_takedown_command(client)) + elif command == "socradar-submit-source-code-leak": + return_results(submit_source_code_leak_takedown_command(client)) + elif command == "socradar-submit-rogue-app": + return_results(submit_rogue_app_takedown_command(client)) + else: + raise NotImplementedError(f"Unknown command {command}") except Exception as e: - demisto.results({ - "Type": entryTypes["error"], - "Contents": f"Failed to execute {demisto.command()} command. Error: {str(e)}", - "ContentsFormat": formats["text"] - }) - + return_error(f"Failed to execute {demisto.command()} command.\nError:\n{str(e)}") """ ENTRY POINT """ From 16a6361dcf23a60b5c86d253acf585cca77510cb Mon Sep 17 00:00:00 2001 From: Radargoger Date: Sun, 29 Jun 2025 17:51:37 +0300 Subject: [PATCH 05/12] Update Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml Co-authored-by: Sapir Malka <44067957+itssapir@users.noreply.github.com> --- .../Integrations/SOCRadarTakedown/SOCRadarTakedown.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml index 1c0503a1388a..6da3e9e3368c 100644 --- a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml +++ b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml @@ -3,7 +3,7 @@ commonfields: version: -1 name: SOCRadar Takedown display: SOCRadar Takedown -category: Threat Intelligence +category: Data Enrichment & Threat Intelligence description: Submit and track takedown requests for phishing domains, social media impersonation, source code leaks, and rogue mobile apps through SOCRadar platform. detaileddescription: | This integration allows you to: From 98abaa123a7500e029a361fa64c5bd779327f1d8 Mon Sep 17 00:00:00 2001 From: Radargoger Date: Sun, 29 Jun 2025 17:55:38 +0300 Subject: [PATCH 06/12] Create SOCRadarTakedown_description.md --- .../SOCRadarTakedown/SOCRadarTakedown_description.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md new file mode 100644 index 000000000000..7dde8b235bef --- /dev/null +++ b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md @@ -0,0 +1,12 @@ +This integration allows you to: +- Submit takedown requests for phishing domains +- Submit takedown requests for social media impersonation +- Submit takedown requests for source code leaks +- Submit takedown requests for rogue mobile apps +- Track the progress of submitted takedown requests + +## Authentication +You need a valid SOCRadar API key and Company ID to use this integration. + +## Rate Limits +Please ensure your API key has adequate rate limits for your usage. From f363abf1d432e4d5265d6cf9c5c1e0b6dfcb4e83 Mon Sep 17 00:00:00 2001 From: Radargoger Date: Sun, 29 Jun 2025 17:56:15 +0300 Subject: [PATCH 07/12] Update Packs/SOCRadarTakedown/pack_metadata.json Co-authored-by: Sapir Malka <44067957+itssapir@users.noreply.github.com> --- Packs/SOCRadarTakedown/pack_metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Packs/SOCRadarTakedown/pack_metadata.json b/Packs/SOCRadarTakedown/pack_metadata.json index 91356622e0dd..2f0968683592 100644 --- a/Packs/SOCRadarTakedown/pack_metadata.json +++ b/Packs/SOCRadarTakedown/pack_metadata.json @@ -6,7 +6,7 @@ "author": "Community Contributor", "url": "", "email": "", - "created": "2024-06-24T00:00:00Z", + "created": "2025-06-24T00:00:00Z", "categories": [ "Threat Intelligence" ], From e119f2bfe3154579fe545377b459d1477ebe4452 Mon Sep 17 00:00:00 2001 From: Radargoger Date: Tue, 1 Jul 2025 17:56:21 +0300 Subject: [PATCH 08/12] Delete Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md --- .../SOCRadarTakedown/SOCRadarTakedown_description.md | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md deleted file mode 100644 index 7dde8b235bef..000000000000 --- a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md +++ /dev/null @@ -1,12 +0,0 @@ -This integration allows you to: -- Submit takedown requests for phishing domains -- Submit takedown requests for social media impersonation -- Submit takedown requests for source code leaks -- Submit takedown requests for rogue mobile apps -- Track the progress of submitted takedown requests - -## Authentication -You need a valid SOCRadar API key and Company ID to use this integration. - -## Rate Limits -Please ensure your API key has adequate rate limits for your usage. From f8df2cdb585b9caf4d726046e499a5cddacd2883 Mon Sep 17 00:00:00 2001 From: radargoger Date: Fri, 4 Jul 2025 09:44:42 +0300 Subject: [PATCH 09/12] Adding SOCRadarTakedown_description.md --- .../SOCRadarTakedown/SOCRadarTakedown_description.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md new file mode 100644 index 000000000000..3ec47ae26287 --- /dev/null +++ b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_description.md @@ -0,0 +1,12 @@ +This integration allows you to: +- Submit takedown requests for phishing domains +- Submit takedown requests for social media impersonation +- Submit takedown requests for source code leaks +- Submit takedown requests for rogue mobile apps +- Track the progress of submitted takedown requests + +## Authentication +You need a valid SOCRadar API key and Company ID to use this integration. + +## Rate Limits +Please ensure your API key has adequate rate limits for your usage. \ No newline at end of file From abc80d12af72426d8a950aa146ed5bf1de4ca7c8 Mon Sep 17 00:00:00 2001 From: Radargoger Date: Mon, 7 Jul 2025 10:34:17 +0300 Subject: [PATCH 10/12] Update SOCRadarTakedown.py --- .../SOCRadarTakedown/SOCRadarTakedown.py | 370 ++++++++++-------- 1 file changed, 203 insertions(+), 167 deletions(-) diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py index e5c02a8a1556..b3fbdee880d5 100644 --- a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py +++ b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.py @@ -13,56 +13,79 @@ """ CONSTANTS """ SOCRADAR_API_ENDPOINT = "https://platform.socradar.com/api" +INTEGRATION_CONTEXT_NAME = "SOCRadarTakedown" MESSAGES = { "BAD_REQUEST_ERROR": "An error occurred while fetching the data.", "AUTHORIZATION_ERROR": "Authorization Error: make sure API Key is correctly set.", "RATE_LIMIT_EXCEED_ERROR": "Rate limit has been exceeded. Please make sure your API key's rate limit is adequate.", + "SUCCESS": "Request submitted successfully", + "FAILED": "Request submission failed" } """ CLIENT CLASS """ + class Client: """ Client class to interact with the SOCRadar Takedown API """ def __init__(self, base_url: str, api_key: str, company_id: str, verify: bool, proxy: bool): - self.base_url = base_url + self.base_url = base_url.rstrip('/') self.api_key = api_key self.company_id = company_id - self.headers = {"API-KEY": self.api_key} + self.headers = { + "API-KEY": self.api_key, + "Content-Type": "application/json" + } self.verify = verify self.proxy = proxy - def check_auth(self) -> Dict[str, Any]: - """Checks if the API key is valid""" - url = f"{self.base_url}/get/company/{self.company_id}/takedown/requests" - + def _http_request(self, method: str, url_suffix: str, json_data: Optional[Dict] = None) -> Dict[str, Any]: + """Generic HTTP request method with proper error handling""" + full_url = f"{self.base_url}{url_suffix}" + try: - response = requests.get( - url, + response = requests.request( + method=method, + url=full_url, headers=self.headers, - verify=self.verify + json=json_data, + verify=self.verify, + proxies=handle_proxy() if self.proxy else None ) + # Handle different HTTP status codes if response.status_code == 401: - raise Exception("Authorization Error: Invalid API Key") + raise DemistoException(MESSAGES["AUTHORIZATION_ERROR"]) elif response.status_code == 429: - raise Exception("Rate limit exceeded") + raise DemistoException(MESSAGES["RATE_LIMIT_EXCEED_ERROR"]) elif response.status_code >= 500: - raise Exception(f"Server Error: {response.status_code}") + raise DemistoException(f"Server Error: {response.status_code} - {response.text}") elif response.status_code >= 400: - raise Exception(f"API Error: {response.status_code}") + raise DemistoException(f"Client Error: {response.status_code} - {response.text}") - return {"is_success": True} + try: + return response.json() + except JSONDecodeError: + return {"status_code": response.status_code, "text": response.text} + except requests.exceptions.Timeout: + raise DemistoException("Request timeout occurred") + except requests.exceptions.ConnectionError: + raise DemistoException("Connection error occurred") except requests.exceptions.RequestException as e: - raise Exception(f"Connection error: {str(e)}") + raise DemistoException(f"Request failed: {str(e)}") + + def test_connection(self) -> Dict[str, Any]: + """Tests API connectivity and authentication""" + url_suffix = f"/get/company/{self.company_id}/takedown/requests" + return self._http_request("GET", url_suffix) - def submit_takedown_request(self, entity: str, request_type: str, abuse_type: str, - notes: str = "", send_alarm: bool = True, email: str = "") -> Dict[str, Any]: - """Generic method to submit takedown requests""" - url = f"{self.base_url}/add/company/{self.company_id}/takedown/request" + def submit_takedown_request(self, entity: str, request_type: str, abuse_type: str, + notes: str = "", send_alarm: bool = True, email: str = "") -> Dict[str, Any]: + """Submit takedown request to SOCRadar API""" + url_suffix = f"/add/company/{self.company_id}/takedown/request" data = { "abuse_type": abuse_type, "entity": entity, @@ -72,23 +95,16 @@ def submit_takedown_request(self, entity: str, request_type: str, abuse_type: st "email": email } - response = requests.post( - url, - json=data, - headers=self.headers, - verify=self.verify - ) + return self._http_request("POST", url_suffix, data) - if response.status_code >= 400: - raise Exception(f"API Error: {response.status_code} - {response.text}") - - return response.json() """ HELPER FUNCTIONS """ + class Validator: @staticmethod def validate_domain(domain_to_validate: str) -> bool: + """Validate domain format""" if not isinstance(domain_to_validate, str) or len(domain_to_validate) > 255: return False if domain_to_validate.endswith("."): @@ -96,14 +112,9 @@ def validate_domain(domain_to_validate: str) -> bool: domain_regex = re.compile(r"(?!-)[A-Z\d-]{1,63}(? bool: - """Basic URL validation""" + """Validate URL format""" url_pattern = re.compile( r'^https?://' # http:// or https:// r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' # domain... @@ -114,16 +125,21 @@ def validate_url(url: str) -> bool: return url_pattern.match(url) is not None @staticmethod - def raise_if_url_not_valid(url: str): - if not Validator.validate_url(url): - raise ValueError(f'URL "{url}" is not a valid URL') + def validate_email(email: str) -> bool: + """Validate email format""" + email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$') + return email_pattern.match(email) is not None + def get_client_from_params() -> Client: """Initialize client from demisto params""" - api_key = demisto.params().get("apikey", "").strip() - company_id = demisto.params().get("company_id", "").strip() - verify_certificate = not demisto.params().get("insecure", False) - proxy = demisto.params().get("proxy", False) + params = demisto.params() + + api_key = params.get("apikey", {}).get("password", "") if isinstance(params.get("apikey"), dict) else params.get("apikey", "") + company_id = params.get("company_id", "").strip() + base_url = params.get("url", SOCRADAR_API_ENDPOINT).strip() + verify_certificate = not params.get("insecure", False) + proxy = params.get("proxy", False) if not api_key: raise ValueError("API Key is required") @@ -131,35 +147,85 @@ def get_client_from_params() -> Client: raise ValueError("Company ID is required") return Client( - base_url=SOCRADAR_API_ENDPOINT, + base_url=base_url, api_key=api_key, company_id=company_id, verify=verify_certificate, proxy=proxy ) + +def create_takedown_command_result(entity: str, entity_type: str, abuse_type: str, + notes: str, send_alarm: bool, raw_response: Dict, + context_prefix: str, key_field: str) -> CommandResults: + """Generic function to create CommandResults for takedown requests""" + + is_success = raw_response.get('is_success', False) or raw_response.get('success', False) + status = MESSAGES["SUCCESS"] if is_success else MESSAGES["FAILED"] + + # Create readable output + readable_output = f"### {entity_type} Takedown Request\n" + readable_output += f"**{key_field}**: {entity}\n" + readable_output += f"**Status**: {status}\n" + readable_output += f"**Abuse Type**: {abuse_type}\n" + + if raw_response.get("message"): + readable_output += f"**Message**: {raw_response.get('message')}\n" + + if notes: + readable_output += f"**Notes**: {notes}\n" + + # Create context output + outputs = { + key_field: entity, + "AbuseType": abuse_type, + "Status": status, + "Message": raw_response.get("message", ""), + "SendAlarm": send_alarm, + "Notes": notes, + "RequestId": raw_response.get("request_id") or raw_response.get("id"), + "Timestamp": raw_response.get("timestamp") or raw_response.get("created_at") + } + + return CommandResults( + outputs_prefix=f"{INTEGRATION_CONTEXT_NAME}.{context_prefix}", + outputs_key_field=key_field, + outputs=outputs, + readable_output=readable_output, + raw_response=raw_response + ) + + """ COMMAND FUNCTIONS """ + def test_module(client: Client) -> str: """Tests API connectivity and authentication""" try: - client.check_auth() + client.test_connection() return "ok" except Exception as e: + demisto.error(f"Test module failed: {str(e)}") return f"Test failed: {str(e)}" + def submit_phishing_domain_takedown_command(client: Client) -> CommandResults: """Submits a takedown request for a phishing domain""" args = demisto.args() - domain = args.get("domain", "") + domain = args.get("domain", "").strip() abuse_type = args.get("abuse_type", "potential_phishing") domain_type = args.get("type", "phishing_domain") notes = args.get("notes", "") - send_alarm = args.get("send_alarm", "true").lower() == "true" - email = args.get("email", "") + send_alarm = argToBoolean(args.get("send_alarm", True)) + email = args.get("email", "").strip() - # Validate domain - Validator.raise_if_domain_not_valid(domain) + # Validation + if not domain: + raise ValueError("Domain is required") + if not Validator.validate_domain(domain): + raise ValueError(f'Domain "{domain}" is not valid') + if email and not Validator.validate_email(email): + raise ValueError(f'Email "{email}" is not valid') # Submit request raw_response = client.submit_takedown_request( @@ -171,42 +237,34 @@ def submit_phishing_domain_takedown_command(client: Client) -> CommandResults: email=email ) - # Prepare output - readable_output = f"### Phishing Domain Takedown Request\n" - readable_output += f"**Domain**: {domain}\n" - readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" - - if raw_response.get("message"): - readable_output += f"**Message**: {raw_response.get('message')}\n" - - outputs = { - "Domain": domain, - "AbuseType": abuse_type, - "Status": "Success" if raw_response.get('is_success', False) else "Failed", - "Message": raw_response.get("message", ""), - "SendAlarm": send_alarm, - "Notes": notes - } - - return CommandResults( - outputs_prefix="SOCRadarTakedown.PhishingDomain", - outputs_key_field="Domain", - outputs=outputs, - readable_output=readable_output, - raw_response=raw_response + return create_takedown_command_result( + entity=domain, + entity_type="Phishing Domain", + abuse_type=abuse_type, + notes=notes, + send_alarm=send_alarm, + raw_response=raw_response, + context_prefix="PhishingDomain", + key_field="Domain" ) + def submit_social_media_impersonation_takedown_command(client: Client) -> CommandResults: """Submits a takedown request for social media impersonation""" args = demisto.args() - url_link = args.get("url", "") + url_link = args.get("url", "").strip() abuse_type = args.get("abuse_type", "impersonating_accounts") notes = args.get("notes", "") - send_alarm = args.get("send_alarm", "true").lower() == "true" - email = args.get("email", "") + send_alarm = argToBoolean(args.get("send_alarm", True)) + email = args.get("email", "").strip() - # Validate URL - Validator.raise_if_url_not_valid(url_link) + # Validation + if not url_link: + raise ValueError("URL is required") + if not Validator.validate_url(url_link): + raise ValueError(f'URL "{url_link}" is not valid') + if email and not Validator.validate_email(email): + raise ValueError(f'Email "{email}" is not valid') # Submit request raw_response = client.submit_takedown_request( @@ -218,42 +276,34 @@ def submit_social_media_impersonation_takedown_command(client: Client) -> Comman email=email ) - # Prepare output - readable_output = f"### Social Media Impersonation Takedown Request\n" - readable_output += f"**URL**: {url_link}\n" - readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" - - if raw_response.get("message"): - readable_output += f"**Message**: {raw_response.get('message')}\n" - - outputs = { - "URL": url_link, - "AbuseType": abuse_type, - "Status": "Success" if raw_response.get('is_success', False) else "Failed", - "Message": raw_response.get("message", ""), - "SendAlarm": send_alarm, - "Notes": notes - } - - return CommandResults( - outputs_prefix="SOCRadarTakedown.SocialMediaImpersonation", - outputs_key_field="URL", - outputs=outputs, - readable_output=readable_output, - raw_response=raw_response + return create_takedown_command_result( + entity=url_link, + entity_type="Social Media Impersonation", + abuse_type=abuse_type, + notes=notes, + send_alarm=send_alarm, + raw_response=raw_response, + context_prefix="SocialMediaImpersonation", + key_field="URL" ) + def submit_source_code_leak_takedown_command(client: Client) -> CommandResults: """Submits a takedown request for leaked source code""" args = demisto.args() - url_link = args.get("url", "") + url_link = args.get("url", "").strip() abuse_type = args.get("abuse_type", "source_code_leak") notes = args.get("notes", "") - send_alarm = args.get("send_alarm", "true").lower() == "true" - email = args.get("email", "") + send_alarm = argToBoolean(args.get("send_alarm", True)) + email = args.get("email", "").strip() - # Validate URL - Validator.raise_if_url_not_valid(url_link) + # Validation + if not url_link: + raise ValueError("URL is required") + if not Validator.validate_url(url_link): + raise ValueError(f'URL "{url_link}" is not valid') + if email and not Validator.validate_email(email): + raise ValueError(f'Email "{email}" is not valid') # Submit request raw_response = client.submit_takedown_request( @@ -265,39 +315,32 @@ def submit_source_code_leak_takedown_command(client: Client) -> CommandResults: email=email ) - # Prepare output - readable_output = f"### Source Code Leak Takedown Request\n" - readable_output += f"**URL**: {url_link}\n" - readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" - - if raw_response.get("message"): - readable_output += f"**Message**: {raw_response.get('message')}\n" - - outputs = { - "URL": url_link, - "AbuseType": abuse_type, - "Status": "Success" if raw_response.get('is_success', False) else "Failed", - "Message": raw_response.get("message", ""), - "SendAlarm": send_alarm, - "Notes": notes - } - - return CommandResults( - outputs_prefix="SOCRadarTakedown.SourceCodeLeak", - outputs_key_field="URL", - outputs=outputs, - readable_output=readable_output, - raw_response=raw_response + return create_takedown_command_result( + entity=url_link, + entity_type="Source Code Leak", + abuse_type=abuse_type, + notes=notes, + send_alarm=send_alarm, + raw_response=raw_response, + context_prefix="SourceCodeLeak", + key_field="URL" ) + def submit_rogue_app_takedown_command(client: Client) -> CommandResults: """Submits a takedown request for a rogue mobile app""" args = demisto.args() - app_info = args.get("app_info", "") + app_info = args.get("app_info", "").strip() abuse_type = args.get("abuse_type", "rogue_mobile_app") notes = args.get("notes", "") - send_alarm = args.get("send_alarm", "true").lower() == "true" - email = args.get("email", "") + send_alarm = argToBoolean(args.get("send_alarm", True)) + email = args.get("email", "").strip() + + # Validation + if not app_info: + raise ValueError("App info is required") + if email and not Validator.validate_email(email): + raise ValueError(f'Email "{email}" is not valid') # Submit request raw_response = client.submit_takedown_request( @@ -309,59 +352,52 @@ def submit_rogue_app_takedown_command(client: Client) -> CommandResults: email=email ) - # Prepare output - readable_output = f"### Rogue App Takedown Request\n" - readable_output += f"**App Info**: {app_info}\n" - readable_output += f"**Status**: {'Success' if raw_response.get('is_success', False) else 'Failed'}\n" - - if raw_response.get("message"): - readable_output += f"**Message**: {raw_response.get('message')}\n" - - outputs = { - "AppInfo": app_info, - "AbuseType": abuse_type, - "Status": "Success" if raw_response.get('is_success', False) else "Failed", - "Message": raw_response.get("message", ""), - "SendAlarm": send_alarm, - "Notes": notes - } - - return CommandResults( - outputs_prefix="SOCRadarTakedown.RogueApp", - outputs_key_field="AppInfo", - outputs=outputs, - readable_output=readable_output, - raw_response=raw_response + return create_takedown_command_result( + entity=app_info, + entity_type="Rogue App", + abuse_type=abuse_type, + notes=notes, + send_alarm=send_alarm, + raw_response=raw_response, + context_prefix="RogueApp", + key_field="AppInfo" ) + """ MAIN FUNCTION """ + def main(): """Main function, parses params and runs command functions""" try: - command = demisto.command() - - if command == "test-module": + demisto.debug(f"Command being called: {demisto.command()}") + + if demisto.command() == "test-module": client = get_client_from_params() result = test_module(client) return_results(result) + else: client = get_client_from_params() - - if command == "socradar-submit-phishing-domain": - return_results(submit_phishing_domain_takedown_command(client)) - elif command == "socradar-submit-social-media-impersonation": - return_results(submit_social_media_impersonation_takedown_command(client)) - elif command == "socradar-submit-source-code-leak": - return_results(submit_source_code_leak_takedown_command(client)) - elif command == "socradar-submit-rogue-app": - return_results(submit_rogue_app_takedown_command(client)) + + commands = { + "socradar-submit-phishing-domain": submit_phishing_domain_takedown_command, + "socradar-submit-social-media-impersonation": submit_social_media_impersonation_takedown_command, + "socradar-submit-source-code-leak": submit_source_code_leak_takedown_command, + "socradar-submit-rogue-app": submit_rogue_app_takedown_command, + } + + command = demisto.command() + if command in commands: + return_results(commands[command](client)) else: - raise NotImplementedError(f"Unknown command {command}") + raise NotImplementedError(f"Command {command} is not implemented") except Exception as e: + demisto.error(f"Failed to execute {demisto.command()} command. Error: {str(e)}") return_error(f"Failed to execute {demisto.command()} command.\nError:\n{str(e)}") + """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): From 78e7aa2d50dddedd017bd2d122b004b3caecf792 Mon Sep 17 00:00:00 2001 From: Radargoger Date: Mon, 7 Jul 2025 10:34:41 +0300 Subject: [PATCH 11/12] Update SOCRadarTakedown.yml --- .../SOCRadarTakedown/SOCRadarTakedown.yml | 159 +++++++++--------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml index 6da3e9e3368c..0d3253b204e5 100644 --- a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml +++ b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml @@ -1,25 +1,17 @@ +category: Data Enrichment & Threat Intelligence commonfields: - id: SOCRadar Takedown + id: SOCRadarTakedown version: -1 -name: SOCRadar Takedown -display: SOCRadar Takedown -category: Data Enrichment & Threat Intelligence -description: Submit and track takedown requests for phishing domains, social media impersonation, source code leaks, and rogue mobile apps through SOCRadar platform. -detaileddescription: | - This integration allows you to: - - Submit takedown requests for phishing domains - - Submit takedown requests for social media impersonation - - Submit takedown requests for source code leaks - - Submit takedown requests for rogue mobile apps - - Track the progress of submitted takedown requests - - ## Authentication - You need a valid SOCRadar API key and Company ID to use this integration. - - ## Rate Limits - Please ensure your API key has adequate rate limits for your usage. - configuration: +- defaultvalue: https://platform.socradar.com + display: Your server URL + name: url + required: true + type: 0 +- display: Use system proxy settings + name: proxy + required: false + type: 8 - display: SOCRadar API Key name: apikey required: true @@ -34,202 +26,211 @@ configuration: name: insecure required: false type: 8 - defaultvalue: false -- display: Use system proxy settings - name: proxy - required: false - type: 8 - defaultvalue: false + defaultvalue: 'false' +description: Submit and track takedown requests for phishing domains, social media impersonation, source code leaks, and rogue mobile apps through SOCRadar platform. +detaileddescription: | + This integration allows you to: + - Submit takedown requests for phishing domains + - Submit takedown requests for social media impersonation + - Submit takedown requests for source code leaks + - Submit takedown requests for rogue mobile apps + - Track the progress of submitted takedown requests + + ## Authentication + You need a valid SOCRadar API key and Company ID to use this integration. + ## Rate Limits + Please ensure your API key has adequate rate limits for your usage. +display: SOCRadarTakedown +name: SOCRadarTakedown script: script: '' type: python subtype: python3 dockerimage: demisto/python3:3.10.13.72123 + runonce: false commands: - name: socradar-submit-phishing-domain - description: Submit a takedown request for a phishing domain + description: Submit a takedown request for a phishing domain. arguments: - name: domain - description: The phishing domain to be taken down + description: The phishing domain to be taken down. required: true - name: abuse_type - description: Type of abuse + description: Type of abuse. defaultValue: potential_phishing predefined: - potential_phishing - confirmed_phishing - name: type - description: Domain type + description: Domain type. defaultValue: phishing_domain predefined: - phishing_domain - lookalike_domain - name: notes - description: Additional notes for the takedown request + description: Additional notes for the takedown request. required: false - name: send_alarm - description: Whether to send alarm notification + description: Whether to send alarm notification. defaultValue: "true" predefined: - "true" - "false" - name: email - description: Email address for notifications + description: Email address for notifications. required: true outputs: - contextPath: SOCRadarTakedown.PhishingDomain.Domain - description: The domain that was reported + description: The domain that was reported. type: String - contextPath: SOCRadarTakedown.PhishingDomain.AbuseType - description: Type of abuse reported + description: Type of abuse reported. type: String - contextPath: SOCRadarTakedown.PhishingDomain.Status - description: Status of the takedown request + description: Status of the takedown request. type: String - contextPath: SOCRadarTakedown.PhishingDomain.Message - description: Response message from the API + description: Response message from the API. type: String - contextPath: SOCRadarTakedown.PhishingDomain.SendAlarm - description: Whether alarm notification is enabled + description: Whether alarm notification is enabled. type: Boolean - contextPath: SOCRadarTakedown.PhishingDomain.Notes - description: Additional notes for the request + description: Additional notes for the request. type: String - name: socradar-submit-social-media-impersonation - description: Submit a takedown request for social media impersonation + description: Submit a takedown request for social media impersonation. arguments: - name: url - description: URL of the impersonating social media account + description: URL of the impersonating social media account. required: true - name: abuse_type - description: Type of abuse + description: Type of abuse. defaultValue: impersonating_accounts predefined: - impersonating_accounts - fake_profiles - name: notes - description: Additional notes for the takedown request + description: Additional notes for the takedown request. required: false - name: send_alarm - description: Whether to send alarm notification + description: Whether to send alarm notification. defaultValue: "true" predefined: - "true" - "false" - name: email - description: Email address for notifications + description: Email address for notifications. required: true outputs: - contextPath: SOCRadarTakedown.SocialMediaImpersonation.URL - description: The URL that was reported + description: The URL that was reported. type: String - contextPath: SOCRadarTakedown.SocialMediaImpersonation.AbuseType - description: Type of abuse reported + description: Type of abuse reported. type: String - contextPath: SOCRadarTakedown.SocialMediaImpersonation.Status - description: Status of the takedown request + description: Status of the takedown request. type: String - contextPath: SOCRadarTakedown.SocialMediaImpersonation.Message - description: Response message from the API + description: Response message from the API. type: String - contextPath: SOCRadarTakedown.SocialMediaImpersonation.SendAlarm - description: Whether alarm notification is enabled + description: Whether alarm notification is enabled. type: Boolean - contextPath: SOCRadarTakedown.SocialMediaImpersonation.Notes - description: Additional notes for the request + description: Additional notes for the request. type: String - name: socradar-submit-source-code-leak - description: Submit a takedown request for leaked source code + description: Submit a takedown request for leaked source code. arguments: - name: url - description: URL where the source code leak is found + description: URL where the source code leak is found. required: true - name: abuse_type - description: Type of abuse + description: Type of abuse. defaultValue: source_code_leak predefined: - source_code_leak - data_leak - name: notes - description: Additional notes for the takedown request + description: Additional notes for the takedown request. required: false - name: send_alarm - description: Whether to send alarm notification + description: Whether to send alarm notification. defaultValue: "true" predefined: - "true" - "false" - name: email - description: Email address for notifications + description: Email address for notifications. required: true outputs: - contextPath: SOCRadarTakedown.SourceCodeLeak.URL - description: The URL that was reported + description: The URL that was reported. type: String - contextPath: SOCRadarTakedown.SourceCodeLeak.AbuseType - description: Type of abuse reported + description: Type of abuse reported. type: String - contextPath: SOCRadarTakedown.SourceCodeLeak.Status - description: Status of the takedown request + description: Status of the takedown request. type: String - contextPath: SOCRadarTakedown.SourceCodeLeak.Message - description: Response message from the API + description: Response message from the API. type: String - contextPath: SOCRadarTakedown.SourceCodeLeak.SendAlarm - description: Whether alarm notification is enabled + description: Whether alarm notification is enabled. type: Boolean - contextPath: SOCRadarTakedown.SourceCodeLeak.Notes - description: Additional notes for the request + description: Additional notes for the request. type: String - name: socradar-submit-rogue-app - description: Submit a takedown request for a rogue mobile app + description: Submit a takedown request for a rogue mobile app. arguments: - name: app_info description: Information about the rogue mobile app (name, store URL, etc.) required: true - name: abuse_type - description: Type of abuse + description: Type of abuse. defaultValue: rogue_mobile_app predefined: - rogue_mobile_app - malicious_app - name: notes - description: Additional notes for the takedown request + description: Additional notes for the takedown request. required: false - name: send_alarm - description: Whether to send alarm notification + description: Whether to send alarm notification. defaultValue: "true" predefined: - "true" - "false" - name: email - description: Email address for notifications + description: Email address for notifications. required: true outputs: - contextPath: SOCRadarTakedown.RogueApp.AppInfo - description: Information about the app that was reported + description: Information about the app that was reported. type: String - contextPath: SOCRadarTakedown.RogueApp.AbuseType - description: Type of abuse reported + description: Type of abuse reported. type: String - contextPath: SOCRadarTakedown.RogueApp.Status - description: Status of the takedown request + description: Status of the takedown request. type: String - contextPath: SOCRadarTakedown.RogueApp.Message - description: Response message from the API + description: Response message from the API. type: String - contextPath: SOCRadarTakedown.RogueApp.SendAlarm - description: Whether alarm notification is enabled + description: Whether alarm notification is enabled. type: Boolean - contextPath: SOCRadarTakedown.RogueApp.Notes - description: Additional notes for the request + description: Additional notes for the request. type: String - runonce: false - ismappable: false - isremotesyncin: false - isremotesyncout: false - -fromversion: 6.0.0 +fromversion: 6.10.0 +marketplaces: +- xsoar +- marketplacev2 tests: - No tests (auto formatted) From d9ebd56a87998ec3ac78f848463351ff8ada54f4 Mon Sep 17 00:00:00 2001 From: radargoger Date: Mon, 7 Jul 2025 11:19:43 +0300 Subject: [PATCH 12/12] change v2 --- .../SOCRadarTakedown/SOCRadarTakedown.yml | 61 +++++++++--------- .../SOCRadarTakedown_image.png | Bin 8523 -> 6576 bytes 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml index 0d3253b204e5..25b023f0d2e6 100644 --- a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml +++ b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown.yml @@ -2,16 +2,25 @@ category: Data Enrichment & Threat Intelligence commonfields: id: SOCRadarTakedown version: -1 +name: SOCRadar Takedown +display: SOCRadar Takedown +category: Data Enrichment & Threat Intelligence +description: Submit and track takedown requests for phishing domains, social media impersonation, source code leaks, and rogue mobile apps through SOCRadar platform. +detaileddescription: | + This integration allows you to: + - Submit takedown requests for phishing domains + - Submit takedown requests for social media impersonation + - Submit takedown requests for source code leaks + - Submit takedown requests for rogue mobile apps + - Track the progress of submitted takedown requests + + ## Authentication + You need a valid SOCRadar API key and Company ID to use this integration. + + ## Rate Limits + Please ensure your API key has adequate rate limits for your usage. + configuration: -- defaultvalue: https://platform.socradar.com - display: Your server URL - name: url - required: true - type: 0 -- display: Use system proxy settings - name: proxy - required: false - type: 8 - display: SOCRadar API Key name: apikey required: true @@ -26,23 +35,13 @@ configuration: name: insecure required: false type: 8 - defaultvalue: 'false' -description: Submit and track takedown requests for phishing domains, social media impersonation, source code leaks, and rogue mobile apps through SOCRadar platform. -detaileddescription: | - This integration allows you to: - - Submit takedown requests for phishing domains - - Submit takedown requests for social media impersonation - - Submit takedown requests for source code leaks - - Submit takedown requests for rogue mobile apps - - Track the progress of submitted takedown requests - - ## Authentication - You need a valid SOCRadar API key and Company ID to use this integration. + defaultvalue: false +- display: Use system proxy settings + name: proxy + required: false + type: 8 + defaultvalue: false - ## Rate Limits - Please ensure your API key has adequate rate limits for your usage. -display: SOCRadarTakedown -name: SOCRadarTakedown script: script: '' type: python @@ -228,9 +227,11 @@ script: - contextPath: SOCRadarTakedown.RogueApp.Notes description: Additional notes for the request. type: String -fromversion: 6.10.0 -marketplaces: -- xsoar -- marketplacev2 + runonce: false + ismappable: false + isremotesyncin: false + isremotesyncout: false + +fromversion: 6.0.0 tests: -- No tests (auto formatted) +- No tests (auto formatted) \ No newline at end of file diff --git a/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_image.png b/Packs/SOCRadarTakedown/Integrations/SOCRadarTakedown/SOCRadarTakedown_image.png index 83fa8264183de2c49be001061e85f0d4a2bb78a0..819ef1d585f090a166877951b6506995f4f32739 100644 GIT binary patch literal 6576 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 UWaCKeza8`Mx@-Q+;V*yse_%VB0{{R3