Skip to content

Commit b24173a

Browse files
Merge branch 'release/3.7.1'
2 parents ce916cf + 9a3ea68 commit b24173a

21 files changed

+1208
-7
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog
22

3+
## [3.7.1](https://github.com/TheHive-Project/Cortex-Analyzers/tree/3.7.1) (2026-01-08)
4+
5+
[Full Changelog](https://github.com/TheHive-Project/Cortex-Analyzers/compare/3.7.0...3.7.1)
6+
7+
**Closed issues:**
8+
9+
- \[FR\] New Analyzer urlDNA.io [\#1303](https://github.com/TheHive-Project/Cortex-Analyzers/issues/1303)
10+
11+
## [3.7.0](https://github.com/TheHive-Project/Cortex-Analyzers/tree/3.7.0) (2026-01-05)
12+
13+
[Full Changelog](https://github.com/TheHive-Project/Cortex-Analyzers/compare/3.6.9...3.7.0)
14+
15+
**Merged pull requests:**
16+
17+
- Rename okta folder to correct vendor name [\#1404](https://github.com/TheHive-Project/Cortex-Analyzers/pull/1404) ([nusantara-self](https://github.com/nusantara-self))
18+
- CI - support build for more than 256 analyzers [\#1406](https://github.com/TheHive-Project/Cortex-Analyzers/pull/1406) ([nusantara-self](https://github.com/nusantara-self))
19+
- Folder/vendor structure & subscription/homepage info improvements [\#1405](https://github.com/TheHive-Project/Cortex-Analyzers/pull/1405) ([nusantara-self](https://github.com/nusantara-self))
20+
- Add CrowdStrike Falcon Threat Intelligence analyzer [\#1397](https://github.com/TheHive-Project/Cortex-Analyzers/pull/1397) ([nusantara-self](https://github.com/nusantara-self))
21+
322
## [3.6.9](https://github.com/TheHive-Project/Cortex-Analyzers/tree/3.6.9) (2026-01-02)
423

524
[Full Changelog](https://github.com/TheHive-Project/Cortex-Analyzers/compare/3.6.8...3.6.9)

analyzers/MSDefenderOffice365/ MSDefenderOffice365_SafeLinksDecoder.json renamed to analyzers/MSDefenderOffice365/MSDefenderOffice365_SafeLinksDecoder.json

File renamed without changes.

analyzers/ValidateObservable/ValidateObservable.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414
"configurationItems": [],
1515
"registration_required": false,
1616
"subscription_required": false,
17-
"free_subscription": false
17+
"free_subscription": false,
18+
"integration_type": "local"
1819
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"name": "UrlDNA_New_Scan",
3+
"author": "urlDNA.io (@urldna); Fabien Bloume, StrangeBee",
4+
"license": "MIT",
5+
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
6+
"version": "0.1.0",
7+
"description": "Perform a new scan on urlDNA.io",
8+
"dataTypeList": ["url"],
9+
"command": "urlDNA.io/urldna_analyzer.py",
10+
"baseConfig": "UrlDNA",
11+
"config": {
12+
"service":"new_scan"
13+
},
14+
"configurationItems": [
15+
{
16+
"name": "key",
17+
"description": "UrlDNA API Key",
18+
"type": "string",
19+
"multi": false,
20+
"required": true
21+
},
22+
{
23+
"name": "private_scan",
24+
"description": "The visibility setting for the new scan: False will make it publicly visible and searchable",
25+
"type": "boolean",
26+
"multi": false,
27+
"required": true,
28+
"defaultValue": true
29+
},
30+
{
31+
"name": "device",
32+
"description": "The device type used for scraping, either DESKTOP or MOBILE. Ensure that the selected Device, User Agent, and Viewport settings are consistent to maintain coherence",
33+
"type": "string",
34+
"multi": false,
35+
"required": false
36+
},
37+
{
38+
"name": "user_agent",
39+
"description": "The browser User Agent used for the scan. Ensure that the selected Device, User Agent, and Viewport settings are consistent to maintain coherence",
40+
"type": "string",
41+
"multi": false,
42+
"required": false
43+
},
44+
{
45+
"name": "viewport_width",
46+
"description": "The screen width of the viewport used for the scan. Ensure that the selected Device, User Agent, and Viewport settings are consistent to maintain coherence",
47+
"type": "number",
48+
"multi": false,
49+
"required": false
50+
},
51+
{
52+
"name": "viewport_height",
53+
"description": "The screen height of the viewport used for the scan. Ensure that the selected Device, User Agent, and Viewport settings are consistent to maintain coherence",
54+
"type": "number",
55+
"multi": false,
56+
"required": false
57+
},
58+
{
59+
"name": "scanned_from",
60+
"description": "Specifies the country from which the new scan is performed. This feature is available exclusively with the API Premium plan.",
61+
"type": "string",
62+
"multi": false,
63+
"required": false
64+
},
65+
{
66+
"name": "waiting_time",
67+
"description": "The waiting time for the page to load during the scan. It can be a number between 5 and 15 seconds",
68+
"type": "number",
69+
"multi": false,
70+
"required": false
71+
}
72+
],
73+
"registration_required": true,
74+
"subscription_required": false,
75+
"free_subscription": true,
76+
"service_homepage": "https://urldna.io"
77+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "UrlDNA_Search",
3+
"author": "urlDNA.io (@urldna); Fabien Bloume, StrangeBee",
4+
"license": "MIT",
5+
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
6+
"version": "0.1.0",
7+
"description": "Perform a search on urlDNA.io for IPs, domains or URLs",
8+
"dataTypeList": ["ip", "domain", "url"],
9+
"command": "urlDNA.io/urldna_analyzer.py",
10+
"baseConfig": "UrlDNA",
11+
"config": {
12+
"service":"search"
13+
},
14+
"configurationItems": [
15+
{
16+
"name": "key",
17+
"description": "UrlDNA API Key",
18+
"type": "string",
19+
"multi": false,
20+
"required": true
21+
}
22+
],
23+
"registration_required": true,
24+
"subscription_required": false,
25+
"free_subscription": true,
26+
"service_homepage": "https://urldna.io"
27+
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
cortexutils
22
requests
3-

analyzers/urlDNA.io/urldna.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#!/usr/bin/env python3
2+
import requests
3+
import time
4+
import base64
5+
6+
7+
class UrlDNAException(Exception):
8+
"""Custom exception for errors related to UrlDNA operations."""
9+
pass
10+
11+
12+
class UrlDNA:
13+
"""A client for interacting with the UrlDNA API."""
14+
15+
BASE_URL = "https://api.urldna.io"
16+
17+
def __init__(self, query, data_type="url"):
18+
"""
19+
Initializes the UrlDNA instance with a query and data type.
20+
21+
:param query: The query to be processed (e.g., URL, domain, or IP).
22+
:param data_type: Type of the query ('url', 'domain', or 'ip').
23+
:raises ValueError: If the query is empty or the data type is unsupported.
24+
"""
25+
if not query:
26+
raise ValueError("Query must be defined.")
27+
if data_type not in ["url", "domain", "ip"]:
28+
raise ValueError(f"Unsupported data type: {data_type}")
29+
30+
self.query = query
31+
self.data_type = data_type
32+
self.session = None
33+
34+
def search(self, api_key):
35+
"""
36+
Performs a search query on the UrlDNA API.
37+
38+
:param api_key: API key for authentication.
39+
:return: A dictionary containing the search results.
40+
"""
41+
self._init_session(api_key)
42+
uri = "/search"
43+
data = {"query": self._build_query()}
44+
response = self.session.post(f"{self.BASE_URL}{uri}", json=data)
45+
response.raise_for_status()
46+
return response.json()
47+
48+
def new_scan(self, api_key, device=None, user_agent=None, viewport_width=None, viewport_height=None,
49+
waiting_time=None, private_scan=False, scanned_from="DEFAULT"):
50+
"""
51+
Initiates a new scan and polls for results until completion.
52+
53+
:param api_key: API key for authentication.
54+
:param device: The device type ('MOBILE' or 'DESKTOP'). Defaults to 'DESKTOP'.
55+
:param user_agent: The user agent string for the scan. Defaults to a common desktop user agent.
56+
:param viewport_width: Width of the viewport. Defaults to 1920.
57+
:param viewport_height: Height of the viewport. Defaults to 1080.
58+
:param waiting_time: Time to wait before starting the scan. Defaults to 5 seconds.
59+
:param private_scan: Whether the scan is private. Defaults to False.
60+
:param scanned_from: The origin of the scan. Defaults to 'DEFAULT'.
61+
:return: A dictionary containing the scan results.
62+
:raises UrlDNAException: If the scan fails or polling times out.
63+
"""
64+
self._init_session(api_key)
65+
try:
66+
scan_id = self._initiate_scan(device, user_agent, viewport_width, viewport_height,
67+
waiting_time, private_scan, scanned_from)
68+
return self._poll_for_result(scan_id)
69+
except requests.RequestException as exc:
70+
raise UrlDNAException(f"HTTP error during scan: {exc}")
71+
except Exception as exc:
72+
raise UrlDNAException(f"Error during scan: {exc}")
73+
74+
def _build_query(self):
75+
"""
76+
Builds the query string based on the data type.
77+
78+
:return: A formatted query string.
79+
"""
80+
if self.data_type == "url":
81+
return f"submitted_url = {self.query}"
82+
if self.data_type == "domain":
83+
return f"domain = {self.query}"
84+
if self.data_type == "ip":
85+
return f"ip = {self.query}"
86+
return self.query
87+
88+
def _initiate_scan(self, device, user_agent, viewport_width, viewport_height, waiting_time,
89+
private_scan, scanned_from):
90+
"""
91+
Sends a request to initiate a new scan.
92+
93+
:param device: The device type for the scan.
94+
:param user_agent: The user agent string for the scan.
95+
:param viewport_width: The viewport width for the scan.
96+
:param viewport_height: The viewport height for the scan.
97+
:param waiting_time: Time to wait before starting the scan.
98+
:param private_scan: Whether the scan is private.
99+
:param scanned_from: The origin of the scan.
100+
:return: The scan ID for the initiated scan.
101+
:raises UrlDNAException: If the scan ID is not returned.
102+
"""
103+
data = {
104+
"submitted_url": self.query,
105+
"device": device or "DESKTOP",
106+
"user_agent": user_agent or (
107+
"Mozilla/5.0 (Windows NT 10.0;Win64;x64) AppleWebKit/537.36 "
108+
"(KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"),
109+
"width": viewport_width or 1920,
110+
"height": viewport_height or 1080,
111+
"waiting_time": waiting_time or 5,
112+
"private_scan": private_scan if private_scan is not None else False,
113+
}
114+
115+
# Only include scanned_from if explicitly set (requires Premium API plan)
116+
if scanned_from:
117+
data["scanned_from"] = scanned_from
118+
119+
response = self.session.post(f"{self.BASE_URL}/scan", json=data)
120+
response.raise_for_status()
121+
scan_id = response.json().get("id")
122+
if not scan_id:
123+
raise UrlDNAException("Scan ID not returned.")
124+
return scan_id
125+
126+
def _poll_for_result(self, scan_id):
127+
"""
128+
Polls the API for the scan results until they are available.
129+
130+
:param scan_id: The scan ID to poll.
131+
:return: A dictionary containing the scan results.
132+
:raises UrlDNAException: If the polling times out.
133+
"""
134+
uri = f"/scan/{scan_id}"
135+
max_attempts = 10
136+
poll_interval = 10
137+
138+
for attempt in range(max_attempts):
139+
if attempt > 0:
140+
time.sleep(poll_interval)
141+
response = self.session.get(f"{self.BASE_URL}{uri}")
142+
response.raise_for_status()
143+
result = response.json()
144+
145+
status = result.get("scan", {}).get("status")
146+
if status not in ["RUNNING", "PENDING"]:
147+
# Convert screenshot and favicon to base64
148+
self._convert_images_to_base64(result)
149+
return result
150+
151+
raise UrlDNAException("Polling timed out before the scan completed.")
152+
153+
def _convert_images_to_base64(self, result):
154+
"""
155+
Downloads images from blob_uri and converts them to base64.
156+
157+
:param result: The scan result dictionary to modify in-place.
158+
"""
159+
# Convert screenshot to base64
160+
screenshot = result.get("screenshot")
161+
if screenshot and screenshot.get("blob_uri"):
162+
try:
163+
img_data = self._download_image(screenshot["blob_uri"])
164+
screenshot["base64_data"] = img_data
165+
except Exception as e:
166+
# If download fails, just log and continue without base64 data
167+
print(f"Failed to download screenshot: {e}")
168+
169+
# Convert favicon to base64
170+
favicon = result.get("favicon")
171+
if favicon and favicon.get("blob_uri"):
172+
try:
173+
img_data = self._download_image(favicon["blob_uri"])
174+
favicon["base64_data"] = img_data
175+
except Exception as e:
176+
# If download fails, just log and continue without base64 data
177+
print(f"Failed to download favicon: {e}")
178+
179+
def _download_image(self, url):
180+
"""
181+
Downloads an image from a URL and returns it as base64.
182+
183+
:param url: The URL of the image to download.
184+
:return: Base64-encoded image data.
185+
"""
186+
response = requests.get(url, timeout=30)
187+
response.raise_for_status()
188+
return base64.b64encode(response.content).decode('utf-8')
189+
190+
def _init_session(self, api_key):
191+
"""
192+
Initializes an HTTP session with the API key for authentication.
193+
194+
:param api_key: The API key for authentication.
195+
"""
196+
if not self.session:
197+
self.session = requests.Session()
198+
self.session.headers.update({
199+
"Content-Type": "application/json",
200+
"User-Agent": "strangebee-thehive",
201+
"Authorization": api_key
202+
})

0 commit comments

Comments
 (0)