diff --git a/analyzers/Watcher/README.md b/analyzers/Watcher/README.md new file mode 100644 index 000000000..295382852 --- /dev/null +++ b/analyzers/Watcher/README.md @@ -0,0 +1,69 @@ +# [Watcher](https://github.com/thalesgroup-cert/Watcher) + +## Watcher Check Domain Analyzer + +### Description +The Watcher Check Domain Analyzer is an Analyzer for TheHive/Cortex that checks if a given domain is already being monitored in the Watcher website monitoring system. + +### Features +- **Check if a domain is monitored**: Verifies whether a specific domain is already being monitored in the Watcher system. + +### Prerequisites +- Access to the Watcher API +- A valid API key for Watcher +- A functional instance of Cortex and TheHive + +### Installation +- Add the configuration files for this analyzer to your Cortex configuration. + +### Configuration +In Cortex, configure the following parameters for the Analyzer: + +| Parameter | Description | Required | Default Value | +|--------------------|--------------------------------------------------------------------|----------|----------------| +| `watcher_url` | URL of Watcher (e.g. `https://example.watcher.local:9002`) | Yes | - | +| `watcher_api_key` | API key for authenticating | Yes | - | + +### Usage +When a domain artifact is submitted to this analyzer, it will: +1. Query the Watcher API to check if the domain is already monitored. +2. Return a report with either the monitoring status of the domain or an indication that it is not yet monitored. + +### Example JSON Response +#### Domain is already monitored +```json +{ + "status": "Monitored", + "Message": "Domain 'example.com' is already monitored in Watcher.", + "ticket_id": "12345" +} +``` + +#### Domain is not monitored +```json +{ + "status": "Not Monitored", + "Message": "Domain 'example.com' is not monitored in Watcher. You can add it using the Watcher responder." +} +``` + +### Template Setup in TheHive + +To customize the display of analyzer results in TheHive, you can use **analyzer templates**. + +Follow these steps to install the templates for the `Watcher_CheckDomain` analyzer: + +1. Navigate to **TheHive** web interface +2. Go to **Admin** > **Entities Management** > **Analyzer templates** +3. Click on **Import templates** +4. Browse to the template directory +5. Select both `short.html` and `long.html` files +6. Click **Import** +7. Make sure the templates are correctly associated with the `Watcher_CheckDomain` analyzer + +Once done, TheHive will use these templates to display the analyzer output with better readability and style. + +### Author + +**Thales Group CERT** - [thalesgroup-cert on GitHub](https://github.com/thalesgroup-cert) +**Ygal NEZRI** - [@ygalnezri](https://github.com/ygalnezri) \ No newline at end of file diff --git a/analyzers/Watcher/Watcher_CheckDomain.json b/analyzers/Watcher/Watcher_CheckDomain.json new file mode 100644 index 000000000..8e31b0ccd --- /dev/null +++ b/analyzers/Watcher/Watcher_CheckDomain.json @@ -0,0 +1,26 @@ +{ + "name": "Watcher_CheckDomain", + "version": "1.3", + "author": "THA-CERT // YNE", + "url": "-", + "license": "AGPL-V3", + "description": "Checks if a domain is monitored in Watcher.", + "dataTypeList": ["domain"], + "command": "Watcher/watcher.py", + "baseConfig": "Watcher", + "configurationItems": [ + { + "name": "watcher_url", + "description": "URL of Watcher.", + "type": "string", + "required": true + }, + { + "name": "watcher_api_key", + "description": "API key used for authenticating requests to Watcher.", + "type": "string", + "required": true + } + ] + } + \ No newline at end of file diff --git a/analyzers/Watcher/requirements.txt b/analyzers/Watcher/requirements.txt new file mode 100644 index 000000000..4a21dbf63 --- /dev/null +++ b/analyzers/Watcher/requirements.txt @@ -0,0 +1,2 @@ +cortexutils +requests \ No newline at end of file diff --git a/analyzers/Watcher/watcher.py b/analyzers/Watcher/watcher.py new file mode 100644 index 000000000..30f18d8c4 --- /dev/null +++ b/analyzers/Watcher/watcher.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +# Author: THA-CERT // YNE + +import requests +import json +from cortexutils.analyzer import Analyzer + + +class Watcher_CheckDomain(Analyzer): + + def __init__(self): + super(Watcher_CheckDomain, self).__init__() + + # Load URL and API key from config + base_url = self.get_param("config.watcher_url", None, "Watcher URL is missing.") + self.watcher_url = f"{base_url.rstrip('/')}/api/site_monitoring/site/" + self.watcher_api_key = self.get_param("config.watcher_api_key", None, "Watcher API key is missing.") + + # Set headers + self.headers = { + "Authorization": f"Token {self.watcher_api_key}", + "Content-Type": "application/json" + } + + def check_domain_status(self, domain): + """Check if the domain is already being monitored in Watcher and return all relevant info.""" + try: + response = requests.get( + self.watcher_url, + headers=self.headers, + verify=False + ) + response.raise_for_status() + sites = response.json() + + # Domain found + for site in sites: + site_domain = str(site.get("domain_name", "")).lower().lstrip("www.") + input_domain = domain.lower().lstrip("www.") + + if site_domain == input_domain: + mx_list = [] + mx_raw = site.get("MX_records") + + # Process MX records if present + if mx_raw: + try: + entries = mx_raw if isinstance(mx_raw, list) else mx_raw.strip("[]").split(",") + + for entry in entries: + if entry and str(entry).strip(): + mx_clean = str(entry).split()[-1].strip(" .'\"]") + if mx_clean: + mx_list.append(mx_clean) + except Exception as e: + self.error(f"Failed to parse MX_records: {str(e)}") + + + return { + "status": "Monitored", + "Message": f"Domain '{domain}' is already monitored by Watcher.", + "Ticket ID": site.get("ticket_id") or "-", + "Ip": site.get("ip") or "-", + "Ip Second": site.get("ip_second") or "-", + "MX Records": mx_list or "-", + "Mail Server": site.get("mail_A_record_ip") or "-" + } + + # Domain not found + return { + "status": "Not Monitored", + "Message": f"Domain '{domain}' is not monitored by Watcher." + } + + except requests.exceptions.RequestException as e: + self.error(f"API request error while checking monitored domains: {str(e)}") + return { + "status": "Error", + "Message": f"Failed to query Watcher: {str(e)}" + } + + def summary(self, raw): + """Generate a summary for TheHive taxonomies.""" + taxonomies = [] + namespace = "Watcher" + predicate = "Check" + status = raw.get("status", "Not Monitored") + + level = "safe" if status == "Monitored" else "info" + taxonomies.append(self.build_taxonomy(level, namespace, predicate, status)) + + return {"taxonomies": taxonomies} + + def artifacts(self, raw): + """Generate artifacts for TheHive.""" + artifacts = [] + + if raw.get("status") != "Monitored": + return artifacts + + # Add IPs + for field in ["Ip", "Ip Second", "Mail Server"]: + ip = raw.get(field) + if ip and ip != "-": + artifacts.append(self.build_artifact("ip", ip)) + + # Add MX Records + for mx in raw.get("MX Records", []): + if mx and mx != "-": + if "." in mx: + parts = mx.split('.') + if len(parts) > 2: + artifacts.append(self.build_artifact("fqdn", mx)) + else: + artifacts.append(self.build_artifact("domain", mx)) + else: + artifacts.append(self.build_artifact("other", mx)) + + return artifacts + + def run(self): + try: + data = self.get_data() + if not data: + self.error("No data received from Cortex. Cannot proceed.") + return + + if isinstance(data, str): + try: + if data.strip() and not data.startswith("{"): + data = json.loads(f'{{"data": "{data}"}}') + except json.JSONDecodeError as e: + self.error(f"Invalid JSON received from Cortex. Input received: {data}. Error: {str(e)}") + return + + domain = data.get("data") + if not domain or not isinstance(domain, str): + self.error("Invalid input: Domain name is missing or not a string.") + return + + result = self.check_domain_status(domain) + self.report(result) + + except Exception as e: + self.error(f"Unexpected error: {str(e)}") + +if __name__ == "__main__": + Watcher_CheckDomain().run() \ No newline at end of file diff --git a/responders/Watcher/README.md b/responders/Watcher/README.md new file mode 100644 index 000000000..3e50215ca --- /dev/null +++ b/responders/Watcher/README.md @@ -0,0 +1,55 @@ +# [Watcher](https://github.com/thalesgroup-cert/Watcher) + +## Watcher Monitor Manager Responder + +### Description +Watcher Monitor Manager is a Responder for TheHive/Cortex that allows adding or removing a domain from monitoring in the Watcher website monitoring module. + +### Features +- **Add a domain to monitoring** (`WatcherAddDomain`) +- **Remove a domain from monitoring** (`WatcherRemoveDomain`) + +### Prerequisites +- Access to the Watcher API +- A valid API key of Watcher +- A functional instance of Cortex and TheHive + +### Installation +- Add the configuration files (`Watcher_Add_Domain.json` and `Watcher_Remove_Domain.json`) to the Cortex configurations. + +### Configuration +In Cortex, configure the following parameters for the Responder: + +| Parameter | Description | Required | Default Value | +|-------------------------|--------------------------------------------------------------------------|----------|----------------| +| `watcher_url` | URL of Watcher (e.g. `https://example.watcher.local:9002`) | Yes | - | +| `watcher_api_key` | API key for authentication | Yes | - | +| `the_hive_custom_field` | Name of the custom field (same as .env variable) | Yes | `watcher-id` | + +### Usage +When an artifact of type `domain` is submitted to this Responder, it will: +1. Extract the Watcher ID from the `customFieldValues` of the alert or case. +2. Perform the requested action (`add` or `remove`) based on the specified service. +3. Return a report indicating the success or failure of the operation. + +### Example JSON Response +#### Adding a Domain +```json +{ + "Message": "Domain 'example.com' successfully added to monitoring with watcher-id: '12345'.", + "WatcherResponse": {"status": "success"} +} +``` + +#### Removing a Domain +```json +{ + "Message": "Domain 'example.com' successfully removed from monitoring.", + "WatcherResponse": {"status": "success"} +} +``` + +### Author + +**Thales Group CERT** - [thalesgroup-cert on GitHub](https://github.com/thalesgroup-cert) +**Ygal NEZRI** - [@ygalnezri](https://github.com/ygalnezri) \ No newline at end of file diff --git a/responders/Watcher/Watcher_Add_Domain.json b/responders/Watcher/Watcher_Add_Domain.json new file mode 100644 index 000000000..2e6c3dafe --- /dev/null +++ b/responders/Watcher/Watcher_Add_Domain.json @@ -0,0 +1,35 @@ +{ + "name": "Watcher_Add_Domain", + "version": "1.2", + "author": "THA-CERT // YNE", + "url": "https://github.com/thalesgroup-cert/Watcher", + "license": "AGPL-V3", + "description": "Add a domain to monitoring in the Website Monitoring module on Watcher.", + "dataTypeList": ["thehive:case_artifact"], + "command": "Watcher/watcher.py", + "baseConfig": "Watcher", + "config": { + "service": "WatcherAddDomain" + }, + "configurationItems": [ + { + "name": "watcher_url", + "description": "URL of Watcher.", + "type": "string", + "required": true + }, + { + "name": "watcher_api_key", + "description": "API key used for authenticating requests to Watcher.", + "type": "string", + "required": true + }, + { + "name": "the_hive_custom_field", + "description": "Name of the custom field (same as .env variable).", + "type": "string", + "required": true, + "defaultValue": "watcher-id" + } + ] +} diff --git a/responders/Watcher/Watcher_Remove_Domain.json b/responders/Watcher/Watcher_Remove_Domain.json new file mode 100644 index 000000000..0c7670fee --- /dev/null +++ b/responders/Watcher/Watcher_Remove_Domain.json @@ -0,0 +1,35 @@ +{ + "name": "Watcher_Remove_Domain", + "version": "1.2", + "author": "THA-CERT // YNE", + "url": "https://github.com/thalesgroup-cert/Watcher", + "license": "AGPL-V3", + "description": "Removes a domain from monitoring in the Website Monitoring module on Watcher.", + "dataTypeList": ["thehive:case_artifact"], + "command": "Watcher/watcher.py", + "baseConfig": "Watcher", + "config": { + "service": "WatcherRemoveDomain" + }, + "configurationItems": [ + { + "name": "watcher_url", + "description": "URL of Watcher.", + "type": "string", + "required": true + }, + { + "name": "watcher_api_key", + "description": "API key used for authenticating requests to Watcher.", + "type": "string", + "required": true + }, + { + "name": "the_hive_custom_field", + "description": "Name of the custom field (same as .env variable).", + "type": "string", + "required": true, + "defaultValue": "watcher-id" + } + ] +} diff --git a/responders/Watcher/requirements.txt b/responders/Watcher/requirements.txt new file mode 100644 index 000000000..4a21dbf63 --- /dev/null +++ b/responders/Watcher/requirements.txt @@ -0,0 +1,2 @@ +cortexutils +requests \ No newline at end of file diff --git a/responders/Watcher/watcher.py b/responders/Watcher/watcher.py new file mode 100644 index 000000000..1ca3f24ba --- /dev/null +++ b/responders/Watcher/watcher.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# Author: THA-CERT // YNE + +import requests +import json +from cortexutils.responder import Responder + +class Watcher_MonitorManager(Responder): + def __init__(self): + super(Watcher_MonitorManager, self).__init__() + + # Load URL and API key from config + base_url = self.get_param("config.watcher_url", None, "Watcher URL is missing.") + self.watcher_url = f"{base_url.rstrip('/')}/api/site_monitoring/site/" + self.watcher_api_key = self.get_param("config.watcher_api_key", None, "Watcher API key is missing.") + self.the_hive_custom_field = self.get_param("config.the_hive_custom_field", "watcher-id", "Custom Field is missing.") + self.service = self.get_param("config.service", None, "Service parameter is missing.") + + # Set headers + self.headers = { + "Authorization": f"Token {self.watcher_api_key}", + "Content-Type": "application/json" + } + + def validate_artifact(self, data): + """Validate if the artifact type is supported (only 'domain' is accepted).""" + domain = data.get("data", None) + artifact_type = data.get("dataType", None) + + if artifact_type != "domain": + return False, None + + return True, domain + + def extract_source_ref(self, data): + """Extract sourceRef (watcher-id) from the provided data, if available.""" + container = data.get("alert") or data.get("case", {}) + custom_fields = container.get("customFieldValues", {}) + return custom_fields.get(self.the_hive_custom_field, None) + + def is_domain_already_monitored(self, domain): + """Check if the domain is already being monitored.""" + try: + response = requests.get( + self.watcher_url, + headers=self.headers, + verify=False + ) + response.raise_for_status() + sites = response.json() + + for site in sites: + if site.get("domain_name") == domain: + self.error(f"Domain '{domain}' already exists in Watcher and is being monitored.") + return True + return False + except requests.exceptions.RequestException as e: + self.error(f"API request error while checking monitored domains: {str(e)}") + return False + + def add_monitor(self, domain, source_ref): + """Add a domain to monitoring.""" + if self.is_domain_already_monitored(domain): + return + + payload = { + "action": "add", + "domain_name": domain, + "ticket_id": source_ref + } + + try: + response = requests.post( + self.watcher_url, + headers=self.headers, + json=payload, + verify=False, + ) + response.raise_for_status() + + response_data = response.json() if response.content else {} + + return { + "Message": f"Domain '{domain}' successfully added to monitoring with {self.the_hive_custom_field}: '{source_ref}'.", + "WatcherResponse": response_data + } + except requests.exceptions.RequestException as e: + self.error(f"Failed to add domain '{domain}' to monitoring: {str(e)}") + + def get_site_id(self, domain): + """Get the site ID associated with a given domain.""" + try: + response = requests.get( + self.watcher_url, + headers=self.headers, + verify=False + ) + response.raise_for_status() + sites = response.json() + + for site in sites: + if site.get("domain_name") == domain: + return site.get("id") + + self.error(f"Domain '{domain}' not found in Watcher.") + except requests.exceptions.RequestException as e: + self.error(f"API request error while fetching site ID for domain '{domain}': {str(e)}") + return None + + def remove_monitor(self, domain, source_ref): + """Remove a domain from monitoring.""" + site_id = self.get_site_id(domain) + if not site_id: + self.error(f"Unable to retrieve site ID for domain '{domain}'.") + + try: + response = requests.delete( + f"{self.watcher_url}{site_id}/", + headers=self.headers, + verify=False + ) + response.raise_for_status() + + response_data = response.json() if response.content else {} + + return { + "Message": f"Domain '{domain}' successfully removed from monitoring.", + "WatcherResponse": response_data + } + except requests.exceptions.RequestException as e: + self.error(f"Failed to remove domain '{domain}' from monitoring: {str(e)}") + + def run(self): + Responder.run(self) + artifact_data = self.get_data() + + is_valid, domain = self.validate_artifact(artifact_data) + if not is_valid: + self.error("Invalid observable data type. Only 'domain' is supported.") + + source_ref = self.extract_source_ref(artifact_data) + if not source_ref: + self.error(f"Missing {self.the_hive_custom_field} in the provided data.") + + if self.service == "WatcherAddDomain": + report = self.add_monitor(domain, source_ref) + elif self.service == "WatcherRemoveDomain": + report = self.remove_monitor(domain, source_ref) + else: + self.error("Invalid service specified.") + + # Send the report + self.report(report) + +if __name__ == "__main__": + Watcher_MonitorManager().run() diff --git a/thehive-templates/Watcher_CheckDomain_1_3/long.html b/thehive-templates/Watcher_CheckDomain_1_3/long.html new file mode 100644 index 000000000..801228a49 --- /dev/null +++ b/thehive-templates/Watcher_CheckDomain_1_3/long.html @@ -0,0 +1,79 @@ + +
Watcher_Add_Domain
+