-
Notifications
You must be signed in to change notification settings - Fork 166
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add support for DigitalOcean provider (#115)
- Loading branch information
1 parent
6eb4606
commit f8673cf
Showing
5 changed files
with
164 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# DigitalOcean | ||
|
||
## Description | ||
The DigitalOcean provider connects to the DigitalOcean API and retrieves domains and records. | ||
It can enumerate all available domains, or alternatively you can supply a comma-separated list of domains to limit | ||
the scope to. | ||
|
||
# Usage | ||
The `--do-api-key` option is used to provide your DigitalOcean API Key. API keys are available from the DigitalOcean | ||
control panel (click API in the sidebar, or [here for a direct link](https://cloud.digitalocean.com/account/api/tokens)). | ||
|
||
The API key should be limited to read-only access. | ||
|
||
The `--do-domains` option is used to limit the domains that are being scanned. Multiple can be provided by separating | ||
each domain with a comma, eg: | ||
`--do-domains first.domain.example,second.domain.example` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Zone Transfer | ||
|
||
## Description | ||
The ZoneTransfer provider connects to a DNS Servers and attempts to fetch all records for a given domain. | ||
|
||
It requires 2 parameters, and both are mandatory. Zone Transfers must fetch a single DNS zone, there is no mechanism in the spec to enumerate all zones on the DNS server. | ||
|
||
The DNS server should permit Zone Transfers to your IP. There is no auth mechanism in the zone transfer spec so it operates on an ip allowlist. You also need TCP Port 53 access to the server, not UDP. | ||
|
||
# Usage | ||
zonetransfer_nameserver, zonetransfer_domain | ||
The `--zonetransfer-nameserver` option is used to provide your DNS server fqdn (such as ns1.domain.com) or DNS server IP. ). | ||
|
||
|
||
The `--zonetransfer-domain` option is used to specify the domain to fetch. This should be the root domain, i.e. a domain of punksecurity.co.uk would be used to fetch all subdomains such as www.punksecurity.co.uk. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import boto3 | ||
import logging | ||
|
||
import requests | ||
|
||
from domain import Domain | ||
|
||
description = "Scan multiple domains by fetching them from Digital Ocean" | ||
|
||
|
||
class DomainNotFoundError(Exception): | ||
def __init__(self, domain): | ||
self.message = "Domain not found: " + domain | ||
super().__init__(self.message) | ||
|
||
|
||
class DoApi: | ||
def __init__(self, api_key): | ||
self.session = requests.session() | ||
self.session.headers.update( | ||
{"Content-Type": "application/json", "Authorization": "Bearer " + api_key} | ||
) | ||
|
||
@staticmethod | ||
def check_response(response: requests.Response): | ||
if response.status_code == 401: | ||
raise ValueError("Invalid API key specified.") | ||
|
||
if response.status_code < 200 or response.status_code >= 300: | ||
raise ValueError("Invalid response received from API: " + response.json()) | ||
|
||
return response | ||
|
||
def make_request(self, endpoint): | ||
return self.session.prepare_request( | ||
requests.Request("GET", "https://api.digitalocean.com/v2/" + endpoint) | ||
) | ||
|
||
def list_domains(self): | ||
req = self.make_request("domains") | ||
|
||
return self.check_response(self.session.send(req)) | ||
|
||
def get_records(self, domain): | ||
req = self.make_request(f"domains/{domain}/records") | ||
res = self.session.send(req) | ||
|
||
if 404 == res.status_code: | ||
raise DomainNotFoundError(domain) | ||
|
||
return self.check_response(res) | ||
|
||
|
||
def convert_records_to_domains(records, root_domain): | ||
buf = {} | ||
for record in records: | ||
if "@" == record["name"]: | ||
continue | ||
|
||
record_name = f"{record['name']}.{root_domain}" | ||
|
||
if record_name not in buf.keys(): | ||
buf[record_name] = {} | ||
|
||
if record["type"] not in buf[record_name].keys(): | ||
buf[record_name][record["type"]] = [] | ||
|
||
if "data" in record.keys(): | ||
buf[record_name][record["type"]].append(record["data"]) | ||
|
||
def extract_records(desired_type): | ||
return [r.rstrip(".") for r in buf[subdomain][desired_type]] | ||
|
||
for subdomain in buf.keys(): | ||
domain = Domain(subdomain.rstrip("."), fetch_standard_records=False) | ||
|
||
if "A" in buf[subdomain].keys(): | ||
domain.A = extract_records("A") | ||
if "AAAA" in buf[subdomain].keys(): | ||
domain.AAAA = extract_records("AAAA") | ||
if "CNAME" in buf[subdomain].keys(): | ||
domain.CNAME = extract_records("CNAME") | ||
if "NS" in buf[subdomain].keys(): | ||
domain.NS = extract_records("NS") | ||
|
||
yield domain | ||
|
||
|
||
def validate_args(do_api_key: str): | ||
if not do_api_key.startswith("dop_v1"): | ||
raise ValueError("DigitalOcean: Invalid API key specified") | ||
|
||
|
||
def fetch_domains(do_api_key: str, do_domains: str = None, **args): # NOSONAR | ||
validate_args(do_api_key) | ||
root_domains = [] | ||
domains = [] | ||
api = DoApi(do_api_key) | ||
|
||
if do_domains is not None and len(do_domains): | ||
root_domains = [domain.strip(" ") for domain in do_domains.split(",")] | ||
else: | ||
resp_data = api.list_domains().json() | ||
root_domains = [domain["name"] for domain in resp_data["domains"]] | ||
|
||
for domain in root_domains: | ||
if "" == domain or domain is None: | ||
continue | ||
|
||
records = api.get_records(domain).json() | ||
domains.extend(convert_records_to_domains(records["domain_records"], domain)) | ||
|
||
return domains |