Skip to content

Commit 284f3e8

Browse files
Merge pull request #1441 from kiaora17/OvhCloud
OVH Cloud Analyzers & Responders
2 parents bd580e3 + 26fb6bf commit 284f3e8

9 files changed

Lines changed: 936 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"name": "OVH_Domain_Check",
3+
"version": "1.0",
4+
"author": "THA-CERT",
5+
"url": "https://github.com/TheHive-Project/Cortex-Analyzers/blob/master/analyzers/OvhCloud",
6+
"license": "AGPL-V3",
7+
"description": "Check if a domain name is available for purchase on OVH Cloud.",
8+
"dataTypeList": ["domain", "fqdn", "url"],
9+
"command": "OvhCloud/ovh_cloud.py",
10+
"baseConfig": "OvhCloud",
11+
"config": {
12+
"service": "OvhDomainCheck"
13+
},
14+
"configurationItems": [
15+
{
16+
"name": "API_endpoint",
17+
"description": "Specify here OVH API's endpoint. Eg: 'ovh-eu', 'ovh-us' or 'ovh-ca'.",
18+
"type": "string",
19+
"multi": false,
20+
"required": true,
21+
"defaultValue": "ovh-eu"
22+
},
23+
{
24+
"name": "API_ovh_subsidiary",
25+
"description": "Specify here which OVH subsidiary where you want to order. Will be 'EU', 'US' or 'CA' by default.",
26+
"type": "string",
27+
"multi": false,
28+
"required": false
29+
},
30+
{
31+
"name": "API_ak",
32+
"description": "Specify here the Application key of your OVH Cloud account.",
33+
"type": "string",
34+
"multi": false,
35+
"required": true
36+
},
37+
{
38+
"name": "API_as",
39+
"description": "Specify here the Application secret of your OVH Cloud account.",
40+
"type": "string",
41+
"multi": false,
42+
"required": true
43+
},
44+
{
45+
"name": "API_cs",
46+
"description": "Specify here the Consumer secret of your OVH Cloud account.",
47+
"type": "string",
48+
"multi": false,
49+
"required": true
50+
},
51+
{
52+
"name": "autoimport_tags",
53+
"description": "Set on 'True' to automatically import analyzer's tags.",
54+
"type": "boolean",
55+
"required": true,
56+
"defaultValue": true
57+
}
58+
]
59+
}

analyzers/OvhCloud/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# OVH Cloud Analyzer
2+
3+
4+
## OVH Domain Check
5+
6+
### Description
7+
*OVH Domain Check* will provide information about a Domain Name availability on OVH Cloud registrar.
8+
It can help to quickly identify if a Domain Name can be purchase on the platform, and at what price.
9+
10+
### Prerequisites
11+
To use this *OVH Domain Check* Analyzer, you will need:
12+
* an active OVHCloud account,
13+
* create a OVHCloud API Keys, with necessary rights. For example:
14+
* post `/order/cart`
15+
* get `/order/cart/*`
16+
17+
### Authors
18+
**Thales Group CERT** - [thalesgroup-cert on GitHub](https://github.com/thalesgroup-cert)

analyzers/OvhCloud/ovh_cloud.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env python3
2+
# Author: THA-CERT
3+
4+
from cortexutils.analyzer import Analyzer
5+
from time import sleep
6+
7+
import ovh
8+
import json
9+
import tldextract
10+
11+
12+
class OvhCloud(Analyzer):
13+
def __init__(self):
14+
Analyzer.__init__(self)
15+
16+
# Set configuration variables
17+
self.service = self.get_param("config.service", None, "Service parameter is missing")
18+
self.autoimport_tags = self.get_param("config.autoimport_tags", True)
19+
self.endpoint = self.get_param("config.API_endpoint", None, "API endpoint is missing")
20+
ovh_subsidiary = self.get_param("config.API_ovh_subsidiary", None)
21+
22+
# Check API endpoint
23+
if self.endpoint not in ["ovh-eu", "ovh-us", "ovh-ca"]:
24+
self.error("Invalid API endpoint, should be 'ovh-eu', 'ovh-us' or 'ovh-ca'")
25+
26+
# Check or set default OVH Subsidiary
27+
ovh_eu = ["CZ", "DE", "ES", "EU", "FI", "FR", "GB", "IE", "IT", "LT", "MA", "NL", "PL", "PT", "SN", "TN"]
28+
ovh_us = ["ASIA", "AU", "CA", "CZ", "DE", "ES", "EU", "FI", "FR", "GB", "IE", "IT", "LT", "MA", "NL", "PL", "PT", "QC", "SG", "SN", "TN", "US", "WE", "WS"]
29+
ovh_ca = ["ASIA", "AU", "CA", "IN", "QC", "SG", "WE", "WS"]
30+
if ovh_subsidiary is not None: # Check
31+
if ovh_subsidiary in locals()[self.endpoint.replace('-', '_')]:
32+
self.ovh_subsidiary = ovh_subsidiary
33+
else:
34+
self.error(f"Invalid OVH Subsidiary '{ovh_subsidiary}' for endpoint {self.endpoint}, should be in: {locals()[self.endpoint.replace('-', '_')]}")
35+
else: # Set default
36+
self.ovh_subsidiary = self.endpoint.split('-')[1].upper()
37+
38+
# Vars init
39+
self.domain = tldextract.TLDExtract(cache_dir=None)(self.get_data()).top_domain_under_public_suffix
40+
if self.domain == "":
41+
self.error("Invalid observable (not containing valid Domain Name)")
42+
43+
# Set API client
44+
self.client = ovh.Client(
45+
endpoint=self.endpoint,
46+
application_key=self.get_param("config.API_ak", None, "API Application key is missing"),
47+
application_secret=self.get_param("config.API_as", None, "API Application secret is missing"),
48+
consumer_key=self.get_param("config.API_cs", None, "API Consumer secret is missing")
49+
)
50+
51+
52+
def run(self):
53+
Analyzer.run(self)
54+
55+
# Init analyzer report
56+
self.output = {
57+
"report": {},
58+
"tags": []
59+
}
60+
self.output["report"]["endpoint"] = self.endpoint
61+
self.output["report"]["subsidiary"] = self.ovh_subsidiary
62+
63+
# Check if a Domain Name is available for purchase
64+
if self.service == "OvhDomainCheck":
65+
# Create a cart
66+
cart_id = self.create_cart()
67+
# Request domain information
68+
domain_info = self.get_domain_info(cart_id)
69+
self.output["domain_info"] = domain_info
70+
if domain_info == []:
71+
domain_info = [{}] # Set empty dict, to proceed next checks without error (Domain Name not available).
72+
73+
# Get Total price
74+
price = None
75+
currency = None
76+
for p in domain_info[0].get("prices", []):
77+
if p.get("label", "") == "TOTAL":
78+
price = p.get("price", {}).get("value", False)
79+
currency = p.get("price", {}).get("currencyCode", False)
80+
81+
if price:
82+
self.output["report"]["price"] = "%.2f" % price + " " + str(currency).lower()
83+
84+
# Get domain offer details ("Pricing Mode")
85+
self.output["report"]["action"] = domain_info[0].get("action", None)
86+
self.output["report"]["pricing_mode"] = domain_info[0].get("pricingMode", None)
87+
88+
# Check if domain is available ('create')
89+
if self.output["report"]["action"] == "create":
90+
self.output["report"]["status"] = "available"
91+
self.output["report"]["message"] = f"{self.domain} is {self.output["report"]["status"]} for {self.output["report"]["price"]}."
92+
self.output["tags"].append("available")
93+
self.output["tags"].append(f"price:{self.output["report"]["price"]}")
94+
else:
95+
# Check if domain is for sale ('transfer-aftermarketX')
96+
if str(self.output["report"]["pricing_mode"]).startswith("transfer-aftermarket"):
97+
self.output["report"]["status"] = "for sale"
98+
self.output["report"]["message"] = f"{self.domain} is {self.output["report"]["status"]} on an aftermarket platform ({self.output["report"]["action"]}) for {self.output["report"]["price"]}."
99+
self.output["tags"].append("for_sale")
100+
self.output["tags"].append(f"price:{self.output["report"]["price"]}")
101+
# Domain is not available
102+
else:
103+
self.output["tags"].append("not_available")
104+
self.output["report"]["status"] = "not available"
105+
self.output["report"]["message"] = f"{self.domain} is {self.output["report"]["status"]}."
106+
107+
self.report(self.output)
108+
109+
110+
def summary(self, raw):
111+
# Default short template
112+
taxonomies = []
113+
level = "info"
114+
namespace = "OVH"
115+
predicate = "Check"
116+
117+
status = raw.get("report", {}).get("status", None)
118+
if status == "available": # Green: available
119+
level = "safe"
120+
elif status is None: # Orange: error while getting status
121+
level = "warning"
122+
123+
taxonomies.append(self.build_taxonomy(level, namespace, predicate, str(status)))
124+
return {"taxonomies": taxonomies}
125+
126+
127+
def operations(self, raw):
128+
operations = []
129+
130+
for tag in raw.get("tags", []):
131+
if self.autoimport_tags and tag not in self.get_param("tags", []):
132+
operations.append(self.build_operation("AddTagToArtifact", tag=f"ovh:{str(tag)}"))
133+
134+
return operations
135+
136+
137+
# Functions for API calls, matching OVH Cloud API documentation (Eg: https://eu.api.ovh.com/console/?branch=v1)
138+
def create_cart(self, retry=3, interval=2):
139+
error = ""
140+
while retry > 0:
141+
try:
142+
r = self.client.post(
143+
"/order/cart",
144+
description="Check domain availability: " + self.domain,
145+
ovhSubsidiary=self.ovh_subsidiary,
146+
)
147+
return r.get("cartId", None)
148+
149+
except Exception as e:
150+
sleep(interval)
151+
retry -= 1
152+
error = e
153+
154+
self.error(f"Error while creating cart: {error}")
155+
156+
157+
def get_domain_info(self, cart_id, retry=3, interval=2):
158+
error = ""
159+
while retry > 0:
160+
try:
161+
r = self.client.get(f"/order/cart/{cart_id}/domain", domain=self.domain)
162+
return r
163+
164+
except Exception as e:
165+
sleep(interval)
166+
retry -= 1
167+
error = e
168+
169+
if str(error).startswith("Some parameters are invalid in the request: Check failed."): # Seen when TLD is not managed by OVH endpoint / subsidiary.
170+
self.output["report"]["details"] = f"TLD possibly not managed by OVH endpoint / subsidiary. Error message: {error}."
171+
self.output["tags"].append("unmanaged_TLD")
172+
return []
173+
else:
174+
self.error(f"Error while getting available offers: {error}")
175+
176+
177+
if __name__ == "__main__":
178+
OvhCloud().run()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
cortexutils
2+
ovh
3+
tldextract
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"name": "OVH_Domain_Order",
3+
"version": "1.0",
4+
"author": "THA-CERT",
5+
"url": "https://github.com/TheHive-Project/Cortex-Analyzers/blob/master/responders/OvhCloud",
6+
"license": "AGPL-V3",
7+
"description": "Buy an available Domain Name on OVH Cloud.",
8+
"dataTypeList": ["thehive:case_artifact"],
9+
"command": "OvhCloud/ovh_cloud.py",
10+
"baseConfig": "OvhCloud",
11+
"config": {
12+
"service": "OvhDomainOrder"
13+
},
14+
"configurationItems": [
15+
{
16+
"name": "API_endpoint",
17+
"description": "Specify here OVH API's endpoint. Eg: 'ovh-eu', 'ovh-us' or 'ovh-ca'.",
18+
"type": "string",
19+
"multi": false,
20+
"required": true,
21+
"defaultValue": "ovh-eu"
22+
},
23+
{
24+
"name": "API_ovh_subsidiary",
25+
"description": "Specify here which OVH subsidiary where you want to order. Will be 'EU', 'US' or 'CA' by default.",
26+
"type": "string",
27+
"multi": false,
28+
"required": false
29+
},
30+
{
31+
"name": "API_ak",
32+
"description": "Specify here the Application key of your OVH Cloud account.",
33+
"type": "string",
34+
"multi": false,
35+
"required": true
36+
},
37+
{
38+
"name": "API_as",
39+
"description": "Specify here the Application secret of your OVH Cloud account.",
40+
"type": "string",
41+
"multi": false,
42+
"required": true
43+
},
44+
{
45+
"name": "API_cs",
46+
"description": "Specify here the Consumer secret of your OVH Cloud account.",
47+
"type": "string",
48+
"multi": false,
49+
"required": true
50+
},
51+
{
52+
"name": "price_limit",
53+
"description": "Maximum allowed price to buy one domain name, WITHOUT Taxes. ⚠ PRICE LIMIT USES OVH SUBSIDIARY DEFAULT CURRENCY ⚠",
54+
"type": "number",
55+
"multi": false,
56+
"required": true
57+
},
58+
{
59+
"name": "required_configuration",
60+
"description": "Set required confirguration values needed by OVH, in order to be able to finalize the order. More info: https://docs.ovh.com/fr/domains/api-order/#recuperation-des-configurations-requises. Format: 'LABEL:VALUE', EG: 'OWNER_CONTACT:/me/contact/1234'",
61+
"type": "string",
62+
"multi": true,
63+
"required": false
64+
},
65+
{
66+
"name": "thehive_url",
67+
"description": "Optionally, specify here the API URL to add informational tags to observable.",
68+
"type": "string",
69+
"multi": false,
70+
"required": false,
71+
"defaultValue": "http://thehive:9000"
72+
},
73+
{
74+
"name": "thehive_token",
75+
"description": "Optionally, specify here the API Key to add informational tags to observable.",
76+
"type": "string",
77+
"multi": false,
78+
"required": false
79+
}
80+
]
81+
}

0 commit comments

Comments
 (0)