diff --git a/ghostwriter/commandcenter/admin.py b/ghostwriter/commandcenter/admin.py
index 5cf2abebe..d33cb33c1 100644
--- a/ghostwriter/commandcenter/admin.py
+++ b/ghostwriter/commandcenter/admin.py
@@ -14,6 +14,7 @@
ReportConfiguration,
SlackConfiguration,
VirusTotalConfiguration,
+ Route53Configuration,
)
admin.site.register(CloudServicesConfiguration, SingletonModelAdmin)
@@ -21,6 +22,7 @@
admin.site.register(NamecheapConfiguration, SingletonModelAdmin)
admin.site.register(SlackConfiguration, SingletonModelAdmin)
admin.site.register(VirusTotalConfiguration, SingletonModelAdmin)
+admin.site.register(Route53Configuration, SingletonModelAdmin)
class ReportConfigurationAdmin(SingletonModelAdmin):
diff --git a/ghostwriter/commandcenter/migrations/0006_route53configuration.py b/ghostwriter/commandcenter/migrations/0006_route53configuration.py
new file mode 100644
index 000000000..771fbe2d0
--- /dev/null
+++ b/ghostwriter/commandcenter/migrations/0006_route53configuration.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.0.10 on 2021-01-08 03:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('commandcenter', '0005_auto_20201102_2207'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Route53Configuration',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('enable', models.BooleanField(default=False)),
+ ('access_key', models.CharField(default='Route53 Access Key', max_length=255)),
+ ('secret_access_key', models.CharField(default='Route53 Secret Access Key', max_length=255)),
+ ],
+ options={
+ 'verbose_name': 'Route53 Configuration',
+ },
+ ),
+ ]
diff --git a/ghostwriter/commandcenter/models.py b/ghostwriter/commandcenter/models.py
index 1e4ddf871..521c9edbb 100644
--- a/ghostwriter/commandcenter/models.py
+++ b/ghostwriter/commandcenter/models.py
@@ -57,6 +57,22 @@ def sanitized_api_key(self):
return sanitize(self.api_key)
+class Route53Configuration(SingletonModel):
+ enable = models.BooleanField(default=False)
+ access_key = models.CharField(max_length=255, default="Route53 Access Key")
+ secret_access_key = models.CharField(max_length=255, default="Route53 Secret Access Key")
+
+ def __str__(self):
+ return "Route53 Configuration"
+
+ class Meta:
+ verbose_name = "Route53 Configuration"
+
+ @property
+ def sanitized_secret_access_key(self):
+ return sanitize(self.secret_access_key)
+
+
class ReportConfiguration(SingletonModel):
enable_borders = models.BooleanField(
default=False, help_text="Enable borders around images in Word documents"
diff --git a/ghostwriter/home/templates/home/management.html b/ghostwriter/home/templates/home/management.html
index c7a1498a4..f30248e43 100644
--- a/ghostwriter/home/templates/home/management.html
+++ b/ghostwriter/home/templates/home/management.html
@@ -18,6 +18,7 @@
{% get_solo "commandcenter.CompanyInformation" as company_config %}
{% get_solo "commandcenter.CloudServicesConfiguration" as cloud_config %}
{% get_solo "commandcenter.NamecheapConfiguration" as namecheap_config %}
+ {% get_solo "commandcenter.Route53Configuration" as route53_config %}
{% get_solo "commandcenter.ReportConfiguration" as report_config %}
{% get_solo "commandcenter.SlackConfiguration" as slack_config %}
{% get_solo "commandcenter.VirusTotalConfiguration" as vt_config %}
@@ -122,6 +123,25 @@
API & Notification Configurations
Disabled |
{% endif %}
+ {% if route53_config.enable %}
+
+ | Route53 API Enabled |
+ {{ route53_config.enable }} |
+
+
+ | Route53 Access Key |
+ {{ route53_config.access_key }} |
+
+
+ | Route53 Secret Access Key |
+ {{ route53_config.sanitized_secret_access_key }} |
+
+ {% else %}
+
+ | Route53 API Enabled |
+ Disabled |
+
+ {% endif %}
@@ -212,6 +232,7 @@ Test Configurations
+
diff --git a/ghostwriter/home/urls.py b/ghostwriter/home/urls.py
index 03a9918d1..d2995b618 100644
--- a/ghostwriter/home/urls.py
+++ b/ghostwriter/home/urls.py
@@ -33,6 +33,11 @@
views.TestNamecheapConnection.as_view(),
name="ajax_test_namecheap",
),
+ path(
+ "ajax/management/test/route53",
+ views.TestRoute53Connection.as_view(),
+ name="ajax_test_route53",
+ ),
path(
"ajax/management/test/slack",
views.TestSlackConnection.as_view(),
diff --git a/ghostwriter/home/views.py b/ghostwriter/home/views.py
index b9996ad21..89ef0875e 100644
--- a/ghostwriter/home/views.py
+++ b/ghostwriter/home/views.py
@@ -274,6 +274,41 @@ def post(self, request, *args, **kwargs):
return JsonResponse(data)
+class TestRoute53Connection(LoginRequiredMixin, UserPassesTestMixin, View):
+ """
+ Create an individual :model:`django_q.Task` under group ``Route53 Test`` with
+ :task:`shepherd.tasks.test_route53` to test the Route53 API configuration stored
+ in :model:`commandcenter.Route53Configuration`.
+ """
+
+ def test_func(self):
+ return self.request.user.is_staff
+
+ def handle_no_permission(self):
+ messages.error(self.request, "You do not have permission to access that")
+ return redirect("home:dashboard")
+
+ def post(self, request, *args, **kwargs):
+ # Add an async task grouped as ``Route53 Test``
+ result = "success"
+ try:
+ task_id = async_task(
+ "ghostwriter.shepherd.tasks.test_route53",
+ self.request.user,
+ group="Route53 Test",
+ )
+ message = "Route53 API test has been successfully queued"
+ except Exception:
+ result = "error"
+ message = "Route53 API test could not be queued"
+
+ data = {
+ "result": result,
+ "message": message,
+ }
+ return JsonResponse(data)
+
+
class TestSlackConnection(LoginRequiredMixin, UserPassesTestMixin, View):
"""
Create an individual :model:`django_q.Task` under group ``Slack Test`` with
diff --git a/ghostwriter/shepherd/tasks.py b/ghostwriter/shepherd/tasks.py
index 1f47bb0cf..babdbe03e 100644
--- a/ghostwriter/shepherd/tasks.py
+++ b/ghostwriter/shepherd/tasks.py
@@ -24,6 +24,7 @@
from ghostwriter.commandcenter.models import (
CloudServicesConfiguration,
NamecheapConfiguration,
+ Route53Configuration,
SlackConfiguration,
VirusTotalConfiguration,
)
@@ -1030,6 +1031,313 @@ def fetch_namecheap_domains():
return domain_changes
+def fetch_route53_domains():
+ """
+ Fetch a list of registered domains for the configured AWS account. A valid set of AWS
+ API credentials must be used. Returns a dictionary containing errors and each domain
+ name paired with change status.
+
+ Result statuses: created, updated, burned, updated & burned
+
+ The returned JSON contains entries for domains like this:
+
+ {
+ 'Domains': [
+ {
+ 'DomainName': 'string',
+ 'AutoRenew': True|False,
+ 'TransferLock': True|False,
+ 'Expiry': datetime(2015, 1, 1),
+ }
+ ],
+ 'NextPageMarker': 'string',
+ 'ResponseMetadata': {
+ 'RequestId': 'string',
+ 'HTTPStatusCode': int,
+ 'HTTPHeaders': {
+ 'x-amzn-requestid': 'string',
+ 'content-type': 'string',
+ 'content-length': 'string',
+ 'date': 'string'
+ },
+ 'RetryAttempts': int
+ }
+ }
+
+ The returned JSON for domain details contains entries for domains like this:
+
+ {
+ 'DomainName': 'string',
+ 'Nameservers': [
+ {
+ 'Name': 'string',
+ 'GlueIps': [
+ 'string',
+ ]
+ },
+ ],
+ 'AutoRenew': True|False,
+ 'AdminContact': {
+ 'FirstName': 'string',
+ 'LastName': 'string',
+ 'ContactType': 'PERSON'|'COMPANY'|'ASSOCIATION'|'PUBLIC_BODY'|'RESELLER',
+ 'OrganizationName': 'string',
+ 'AddressLine1': 'string',
+ 'AddressLine2': 'string',
+ 'City': 'string',
+ 'State': 'string',
+ 'CountryCode': 'AD'|'AE'|'AF'|'AG'|'AI'|'AL'|'AM'|'AN'|'AO'|'AQ'|'AR'|'AS'|'AT'|'AU'|'AW'|'AZ'|'BA'|'BB'|'BD'|'BE'|'BF'|'BG'|'BH'|'BI'|'BJ'|'BL'|'BM'|'BN'|'BO'|'BR'|'BS'|'BT'|'BW'|'BY'|'BZ'|'CA'|'CC'|'CD'|'CF'|'CG'|'CH'|'CI'|'CK'|'CL'|'CM'|'CN'|'CO'|'CR'|'CU'|'CV'|'CX'|'CY'|'CZ'|'DE'|'DJ'|'DK'|'DM'|'DO'|'DZ'|'EC'|'EE'|'EG'|'ER'|'ES'|'ET'|'FI'|'FJ'|'FK'|'FM'|'FO'|'FR'|'GA'|'GB'|'GD'|'GE'|'GH'|'GI'|'GL'|'GM'|'GN'|'GQ'|'GR'|'GT'|'GU'|'GW'|'GY'|'HK'|'HN'|'HR'|'HT'|'HU'|'ID'|'IE'|'IL'|'IM'|'IN'|'IQ'|'IR'|'IS'|'IT'|'JM'|'JO'|'JP'|'KE'|'KG'|'KH'|'KI'|'KM'|'KN'|'KP'|'KR'|'KW'|'KY'|'KZ'|'LA'|'LB'|'LC'|'LI'|'LK'|'LR'|'LS'|'LT'|'LU'|'LV'|'LY'|'MA'|'MC'|'MD'|'ME'|'MF'|'MG'|'MH'|'MK'|'ML'|'MM'|'MN'|'MO'|'MP'|'MR'|'MS'|'MT'|'MU'|'MV'|'MW'|'MX'|'MY'|'MZ'|'NA'|'NC'|'NE'|'NG'|'NI'|'NL'|'NO'|'NP'|'NR'|'NU'|'NZ'|'OM'|'PA'|'PE'|'PF'|'PG'|'PH'|'PK'|'PL'|'PM'|'PN'|'PR'|'PT'|'PW'|'PY'|'QA'|'RO'|'RS'|'RU'|'RW'|'SA'|'SB'|'SC'|'SD'|'SE'|'SG'|'SH'|'SI'|'SK'|'SL'|'SM'|'SN'|'SO'|'SR'|'ST'|'SV'|'SY'|'SZ'|'TC'|'TD'|'TG'|'TH'|'TJ'|'TK'|'TL'|'TM'|'TN'|'TO'|'TR'|'TT'|'TV'|'TW'|'TZ'|'UA'|'UG'|'US'|'UY'|'UZ'|'VA'|'VC'|'VE'|'VG'|'VI'|'VN'|'VU'|'WF'|'WS'|'YE'|'YT'|'ZA'|'ZM'|'ZW',
+ 'ZipCode': 'string',
+ 'PhoneNumber': 'string',
+ 'Email': 'string',
+ 'Fax': 'string',
+ 'ExtraParams': [
+ {
+ 'Name': 'DUNS_NUMBER'|'BRAND_NUMBER'|'BIRTH_DEPARTMENT'|'BIRTH_DATE_IN_YYYY_MM_DD'|'BIRTH_COUNTRY'|'BIRTH_CITY'|'DOCUMENT_NUMBER'|'AU_ID_NUMBER'|'AU_ID_TYPE'|'CA_LEGAL_TYPE'|'CA_BUSINESS_ENTITY_TYPE'|'CA_LEGAL_REPRESENTATIVE'|'CA_LEGAL_REPRESENTATIVE_CAPACITY'|'ES_IDENTIFICATION'|'ES_IDENTIFICATION_TYPE'|'ES_LEGAL_FORM'|'FI_BUSINESS_NUMBER'|'FI_ID_NUMBER'|'FI_NATIONALITY'|'FI_ORGANIZATION_TYPE'|'IT_NATIONALITY'|'IT_PIN'|'IT_REGISTRANT_ENTITY_TYPE'|'RU_PASSPORT_DATA'|'SE_ID_NUMBER'|'SG_ID_NUMBER'|'VAT_NUMBER'|'UK_CONTACT_TYPE'|'UK_COMPANY_NUMBER',
+ 'Value': 'string'
+ },
+ ]
+ },
+ 'RegistrantContact': {
+ 'FirstName': 'string',
+ 'LastName': 'string',
+ 'ContactType': 'PERSON'|'COMPANY'|'ASSOCIATION'|'PUBLIC_BODY'|'RESELLER',
+ 'OrganizationName': 'string',
+ 'AddressLine1': 'string',
+ 'AddressLine2': 'string',
+ 'City': 'string',
+ 'State': 'string',
+ 'CountryCode': 'AD'|'AE'|'AF'|'AG'|'AI'|'AL'|'AM'|'AN'|'AO'|'AQ'|'AR'|'AS'|'AT'|'AU'|'AW'|'AZ'|'BA'|'BB'|'BD'|'BE'|'BF'|'BG'|'BH'|'BI'|'BJ'|'BL'|'BM'|'BN'|'BO'|'BR'|'BS'|'BT'|'BW'|'BY'|'BZ'|'CA'|'CC'|'CD'|'CF'|'CG'|'CH'|'CI'|'CK'|'CL'|'CM'|'CN'|'CO'|'CR'|'CU'|'CV'|'CX'|'CY'|'CZ'|'DE'|'DJ'|'DK'|'DM'|'DO'|'DZ'|'EC'|'EE'|'EG'|'ER'|'ES'|'ET'|'FI'|'FJ'|'FK'|'FM'|'FO'|'FR'|'GA'|'GB'|'GD'|'GE'|'GH'|'GI'|'GL'|'GM'|'GN'|'GQ'|'GR'|'GT'|'GU'|'GW'|'GY'|'HK'|'HN'|'HR'|'HT'|'HU'|'ID'|'IE'|'IL'|'IM'|'IN'|'IQ'|'IR'|'IS'|'IT'|'JM'|'JO'|'JP'|'KE'|'KG'|'KH'|'KI'|'KM'|'KN'|'KP'|'KR'|'KW'|'KY'|'KZ'|'LA'|'LB'|'LC'|'LI'|'LK'|'LR'|'LS'|'LT'|'LU'|'LV'|'LY'|'MA'|'MC'|'MD'|'ME'|'MF'|'MG'|'MH'|'MK'|'ML'|'MM'|'MN'|'MO'|'MP'|'MR'|'MS'|'MT'|'MU'|'MV'|'MW'|'MX'|'MY'|'MZ'|'NA'|'NC'|'NE'|'NG'|'NI'|'NL'|'NO'|'NP'|'NR'|'NU'|'NZ'|'OM'|'PA'|'PE'|'PF'|'PG'|'PH'|'PK'|'PL'|'PM'|'PN'|'PR'|'PT'|'PW'|'PY'|'QA'|'RO'|'RS'|'RU'|'RW'|'SA'|'SB'|'SC'|'SD'|'SE'|'SG'|'SH'|'SI'|'SK'|'SL'|'SM'|'SN'|'SO'|'SR'|'ST'|'SV'|'SY'|'SZ'|'TC'|'TD'|'TG'|'TH'|'TJ'|'TK'|'TL'|'TM'|'TN'|'TO'|'TR'|'TT'|'TV'|'TW'|'TZ'|'UA'|'UG'|'US'|'UY'|'UZ'|'VA'|'VC'|'VE'|'VG'|'VI'|'VN'|'VU'|'WF'|'WS'|'YE'|'YT'|'ZA'|'ZM'|'ZW',
+ 'ZipCode': 'string',
+ 'PhoneNumber': 'string',
+ 'Email': 'string',
+ 'Fax': 'string',
+ 'ExtraParams': [
+ {
+ 'Name': 'DUNS_NUMBER'|'BRAND_NUMBER'|'BIRTH_DEPARTMENT'|'BIRTH_DATE_IN_YYYY_MM_DD'|'BIRTH_COUNTRY'|'BIRTH_CITY'|'DOCUMENT_NUMBER'|'AU_ID_NUMBER'|'AU_ID_TYPE'|'CA_LEGAL_TYPE'|'CA_BUSINESS_ENTITY_TYPE'|'CA_LEGAL_REPRESENTATIVE'|'CA_LEGAL_REPRESENTATIVE_CAPACITY'|'ES_IDENTIFICATION'|'ES_IDENTIFICATION_TYPE'|'ES_LEGAL_FORM'|'FI_BUSINESS_NUMBER'|'FI_ID_NUMBER'|'FI_NATIONALITY'|'FI_ORGANIZATION_TYPE'|'IT_NATIONALITY'|'IT_PIN'|'IT_REGISTRANT_ENTITY_TYPE'|'RU_PASSPORT_DATA'|'SE_ID_NUMBER'|'SG_ID_NUMBER'|'VAT_NUMBER'|'UK_CONTACT_TYPE'|'UK_COMPANY_NUMBER',
+ 'Value': 'string'
+ },
+ ]
+ },
+ 'TechContact': {
+ 'FirstName': 'string',
+ 'LastName': 'string',
+ 'ContactType': 'PERSON'|'COMPANY'|'ASSOCIATION'|'PUBLIC_BODY'|'RESELLER',
+ 'OrganizationName': 'string',
+ 'AddressLine1': 'string',
+ 'AddressLine2': 'string',
+ 'City': 'string',
+ 'State': 'string',
+ 'CountryCode': 'AD'|'AE'|'AF'|'AG'|'AI'|'AL'|'AM'|'AN'|'AO'|'AQ'|'AR'|'AS'|'AT'|'AU'|'AW'|'AZ'|'BA'|'BB'|'BD'|'BE'|'BF'|'BG'|'BH'|'BI'|'BJ'|'BL'|'BM'|'BN'|'BO'|'BR'|'BS'|'BT'|'BW'|'BY'|'BZ'|'CA'|'CC'|'CD'|'CF'|'CG'|'CH'|'CI'|'CK'|'CL'|'CM'|'CN'|'CO'|'CR'|'CU'|'CV'|'CX'|'CY'|'CZ'|'DE'|'DJ'|'DK'|'DM'|'DO'|'DZ'|'EC'|'EE'|'EG'|'ER'|'ES'|'ET'|'FI'|'FJ'|'FK'|'FM'|'FO'|'FR'|'GA'|'GB'|'GD'|'GE'|'GH'|'GI'|'GL'|'GM'|'GN'|'GQ'|'GR'|'GT'|'GU'|'GW'|'GY'|'HK'|'HN'|'HR'|'HT'|'HU'|'ID'|'IE'|'IL'|'IM'|'IN'|'IQ'|'IR'|'IS'|'IT'|'JM'|'JO'|'JP'|'KE'|'KG'|'KH'|'KI'|'KM'|'KN'|'KP'|'KR'|'KW'|'KY'|'KZ'|'LA'|'LB'|'LC'|'LI'|'LK'|'LR'|'LS'|'LT'|'LU'|'LV'|'LY'|'MA'|'MC'|'MD'|'ME'|'MF'|'MG'|'MH'|'MK'|'ML'|'MM'|'MN'|'MO'|'MP'|'MR'|'MS'|'MT'|'MU'|'MV'|'MW'|'MX'|'MY'|'MZ'|'NA'|'NC'|'NE'|'NG'|'NI'|'NL'|'NO'|'NP'|'NR'|'NU'|'NZ'|'OM'|'PA'|'PE'|'PF'|'PG'|'PH'|'PK'|'PL'|'PM'|'PN'|'PR'|'PT'|'PW'|'PY'|'QA'|'RO'|'RS'|'RU'|'RW'|'SA'|'SB'|'SC'|'SD'|'SE'|'SG'|'SH'|'SI'|'SK'|'SL'|'SM'|'SN'|'SO'|'SR'|'ST'|'SV'|'SY'|'SZ'|'TC'|'TD'|'TG'|'TH'|'TJ'|'TK'|'TL'|'TM'|'TN'|'TO'|'TR'|'TT'|'TV'|'TW'|'TZ'|'UA'|'UG'|'US'|'UY'|'UZ'|'VA'|'VC'|'VE'|'VG'|'VI'|'VN'|'VU'|'WF'|'WS'|'YE'|'YT'|'ZA'|'ZM'|'ZW',
+ 'ZipCode': 'string',
+ 'PhoneNumber': 'string',
+ 'Email': 'string',
+ 'Fax': 'string',
+ 'ExtraParams': [
+ {
+ 'Name': 'DUNS_NUMBER'|'BRAND_NUMBER'|'BIRTH_DEPARTMENT'|'BIRTH_DATE_IN_YYYY_MM_DD'|'BIRTH_COUNTRY'|'BIRTH_CITY'|'DOCUMENT_NUMBER'|'AU_ID_NUMBER'|'AU_ID_TYPE'|'CA_LEGAL_TYPE'|'CA_BUSINESS_ENTITY_TYPE'|'CA_LEGAL_REPRESENTATIVE'|'CA_LEGAL_REPRESENTATIVE_CAPACITY'|'ES_IDENTIFICATION'|'ES_IDENTIFICATION_TYPE'|'ES_LEGAL_FORM'|'FI_BUSINESS_NUMBER'|'FI_ID_NUMBER'|'FI_NATIONALITY'|'FI_ORGANIZATION_TYPE'|'IT_NATIONALITY'|'IT_PIN'|'IT_REGISTRANT_ENTITY_TYPE'|'RU_PASSPORT_DATA'|'SE_ID_NUMBER'|'SG_ID_NUMBER'|'VAT_NUMBER'|'UK_CONTACT_TYPE'|'UK_COMPANY_NUMBER',
+ 'Value': 'string'
+ },
+ ]
+ },
+ 'AdminPrivacy': True|False,
+ 'RegistrantPrivacy': True|False,
+ 'TechPrivacy': True|False,
+ 'RegistrarName': 'string',
+ 'WhoIsServer': 'string',
+ 'RegistrarUrl': 'string',
+ 'AbuseContactEmail': 'string',
+ 'AbuseContactPhone': 'string',
+ 'RegistryDomainId': 'string',
+ 'CreationDate': datetime(2015, 1, 1),
+ 'UpdatedDate': datetime(2015, 1, 1),
+ 'ExpirationDate': datetime(2015, 1, 1),
+ 'Reseller': 'string',
+ 'DnsSec': 'string',
+ 'StatusList': [
+ 'string',
+ ]
+ }
+ """
+ domains_list = []
+ domain_changes = {}
+ domain_changes["errors"] = {}
+ domain_changes["updates"] = {}
+
+ logger.info(
+ "Starting Route53 synchronization task at %s", datetime.datetime.now()
+ )
+
+ # Fetch route53 API keys and tokens
+ route53_config = Route53Configuration.get_solo()
+
+ # Set timezone for dates to UTC
+ utc = pytz.UTC
+
+ try:
+ client = boto3.client(
+ "route53domains",
+ region_name="us-east-1",
+ aws_access_key_id=route53_config.access_key,
+ aws_secret_access_key=route53_config.secret_access_key,
+ )
+
+ domain_req = client.list_domains(MaxItems=100)
+ if domain_req["ResponseMetadata"]["HTTPStatusCode"] == 200:
+ while True:
+ for domain in domain_req["Domains"]:
+ domains_list.append(domain)
+ if "NextPageMarker" in domain_req:
+ domain_req = client.list_domains(MaxItems=100, Marker=domain_req["NextPageMarker"])
+ else:
+ break
+ else:
+ logger.error(
+ "Route53 returned a %s response", domain_req["ResponseMetadata"]["HTTPStatusCode"]
+ )
+ domain_changes["errors"][
+ "route53"
+ ] = "Route53 returned a {status_code} response".format(
+ status_code=domain_req["ResponseMetadata"]["HTTPStatusCode"]
+ )
+ return domain_changes
+ except ClientError:
+ logger.error("Route53 API request failed")
+ domain_changes["errors"][
+ "route53"
+ ] = "The Route53 API request failed: {traceback}".format(traceback=traceback.format_exc())
+ return domain_changes
+
+ # There's a chance no domains are returned if the provided usernames don't have any domains
+ if domains_list:
+ # Get the current list of Route53 domains in the library
+ domain_queryset = Domain.objects.filter(registrar="Route53")
+ expired_status = DomainStatus.objects.get(domain_status="Expired")
+ for domain in domain_queryset:
+ # Check if a domain in the library is _not_ in the Route53 response
+ if not any(d["DomainName"] == domain.name for d in domains_list):
+ # Domains not found in Route53 have expired and fallen off the account
+ if not domain.expired:
+ logger.info(
+ "Domain %s is not in the Route53 data so it is now marked as expired",
+ domain.name,
+ )
+ # Mark the domain as Expired
+ domain_changes["updates"][domain.id] = {}
+ domain_changes["updates"][domain.id]["domain"] = domain.name
+ domain_changes["updates"][domain.id]["change"] = "expired"
+ entry = {}
+ domain.expired = True
+ domain.auto_renew = False
+ domain.domain_status = expired_status
+ # If the domain expiration date is in the future, adjust it
+ if domain.expiration >= date.today():
+ domain.expiration = domain.expiration - datetime.timedelta(
+ days=365
+ )
+ try:
+ for attr, value in entry.items():
+ setattr(domain, attr, value)
+ domain.save()
+ except Exception:
+ trace = traceback.format_exc()
+ domain_changes["errors"][
+ domain
+ ] = "Failed to update the entry for {domain}: {traceback}".format(
+ domain=domain, traceback=trace
+ )
+ logger.exception(
+ "Failed to update the entry for %s", domain.name
+ )
+ pass
+ instance = DomainNote.objects.create(
+ domain=domain,
+ note="Automatically set to Expired because the domain did not appear in Route53 during a sync.",
+ )
+
+ # perform domain updates
+ for domain in domains_list:
+ logger.info("Domain %s is now being processed", domain["DomainName"])
+
+ # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/route53domains.html#Route53Domains.Client.get_domain_detail
+ domain_details = client.get_domain_detail(DomainName=domain["DomainName"])
+
+ # Prepare domain attributes for Domain model
+ entry = {}
+ entry["name"] = domain["DomainName"]
+ entry["registrar"] = "Route53"
+
+ # Set the WHOIS status based on WhoisGuard
+ if domain["Expiry"].replace(tzinfo=utc) < datetime.datetime.now().replace(tzinfo=utc):
+ entry["expired"] = True
+ # Expired domains have WhoisGuard set to ``NOTPRESENT``
+ entry["whois_status"] = WhoisStatus.objects.get(pk=2)
+ else:
+ try:
+ whois_status = "Disabled"
+ if domain_details["AdminPrivacy"] \
+ and domain_details["RegistrantPrivacy"] \
+ and domain_details["TechPrivacy"]:
+ whois_status = "Enabled"
+
+ entry["whois_status"] = WhoisStatus.objects.get(
+ whois_status__iexact=whois_status
+ )
+ # Anything not ``Enabled`` or ``Disabled``, set to ``Unknown``
+ except Exception:
+ logger.exception(
+ "Route53 WHOIS status (%s) was not found in the database, so defaulted to `Unknown`",
+ domain["WhoisGuard"].capitalize(),
+ )
+ entry["whois_status"] = WhoisStatus.objects.get(pk=3)
+
+ # Set AutoRenew status
+ if not domain["AutoRenew"]:
+ entry["auto_renew"] = False
+
+ # Convert Route53 dates to Django
+ entry["creation"] = domain_details["CreationDate"].strftime("%Y-%m-%d")
+ entry["expiration"] = domain_details["ExpirationDate"].strftime("%Y-%m-%d")
+
+ # Update or create the domain record with assigned attrs
+ try:
+ instance, created = Domain.objects.update_or_create(
+ name=domain.get("DomainName"), defaults=entry
+ )
+ for attr, value in entry.items():
+ setattr(instance, attr, value)
+
+ logger.debug(
+ "Domain %s is being saved with this data: %s", domain["DomainName"], entry
+ )
+ instance.save()
+
+ # Add entry to domain change tracking dict
+ domain_changes["updates"][instance.id] = {}
+ domain_changes["updates"][instance.id]["domain"] = domain["DomainName"]
+ if created:
+ domain_changes["updates"][instance.id]["change"] = "created"
+ else:
+ domain_changes["updates"][instance.id]["change"] = "updated"
+ except Exception:
+ trace = traceback.format_exc()
+ logger.exception(
+ "Encountered an exception while trying to create or update %s",
+ domain["DomainName"],
+ )
+ domain_changes["errors"][domain["DomainName"]] = {}
+ domain_changes["errors"][domain["DomainName"]]["error"] = trace
+ logger.info(
+ "Route53 synchronization completed at %s with these changes:\n%s",
+ datetime.datetime.now(),
+ domain_changes,
+ )
+ else:
+ logger.warning("No domains were returned for the provided Route53 account!")
+
+ return domain_changes
+
+
def months_between(date1, date2):
"""
Compare two dates and return the number of months beetween them.
@@ -1617,6 +1925,59 @@ def test_namecheap(user):
return {"result": level, "message": message}
+def test_route53(user):
+ """
+ Test the Route53 API configuration stored in :model:`commandcenter.Route53Configuration`.
+ """
+ route53_config = Route53Configuration.get_solo()
+ level = "error"
+ logger.info("Starting Route53 API test at %s", datetime.datetime.now())
+ try:
+ client = boto3.client(
+ "route53domains",
+ region_name="us-east-1",
+ aws_access_key_id=route53_config.access_key,
+ aws_secret_access_key=route53_config.secret_access_key,
+ )
+
+ domain_req = client.list_domains(MaxItems=100)
+
+ # Check if request returned a 200 OK
+ status_code = domain_req["ResponseMetadata"]["HTTPStatusCode"]
+ if status_code == 200:
+ level = "success"
+ message = "Successfully authenticated to Route53"
+ else:
+ logger.error(
+ "Route53 returned HTTP code %s in its response",
+ status_code
+ )
+ message = f"Route53 returned HTTP code {status_code} in its response"
+ except Exception:
+ trace = traceback.format_exc()
+ logger.exception("Route53 API request failed")
+ message = f"The Route53 API request failed: {trace}"
+
+ # Send a message to the requesting user
+ async_to_sync(channel_layer.group_send)(
+ "notify_{}".format(user),
+ {
+ "type": "message",
+ "message": {
+ "message": message,
+ "level": level,
+ "title": "Route53 Test Complete",
+ },
+ },
+ )
+
+ logger.info(
+ "Test of the Route53 API configuration completed at %s",
+ datetime.datetime.now(),
+ )
+ return {"result": level, "message": message}
+
+
def test_slack_webhook(user):
"""
Test the Slack Webhook configuration stored in :model:`commandcenter.SlackConfiguration`.
diff --git a/ghostwriter/shepherd/templates/shepherd/update.html b/ghostwriter/shepherd/templates/shepherd/update.html
index 1546bd94c..1f6e47728 100644
--- a/ghostwriter/shepherd/templates/shepherd/update.html
+++ b/ghostwriter/shepherd/templates/shepherd/update.html
@@ -57,6 +57,43 @@ Pull Domains from Namecheap
+
+ {% if enable_route53 %}
+ Pull Domains from Route53
+
+
+ The domain library sync with Route53 was last requested on:
+
+ {{ route53_last_update_requested }}
+
+
+ {% if route53_last_update_completed %}
+ {% if route53_last_update_completed == 'Failed' %}
+ Request Status: {{ route53_last_update_completed }}
+ {% if cat_last_result %}
+
+ Error:
+
+ {{ route53_last_result }}
+
+
+ {% endif %}
+ {% else %}
+ {% if route53_last_update_completed %}
+ Request Status: Completed on {{ route53_last_update_completed }} in {{ route53_last_update_time }} minutes
+ {% endif %}
+ {% endif %}
+ {% endif %}
+
+
+ {% endif %}
+
+
+
Domain Categories
diff --git a/ghostwriter/shepherd/urls.py b/ghostwriter/shepherd/urls.py
index ebe2a11f6..08bc03060 100644
--- a/ghostwriter/shepherd/urls.py
+++ b/ghostwriter/shepherd/urls.py
@@ -76,6 +76,11 @@
views.RegistrarSyncNamecheap.as_view(),
name="ajax_update_namecheap",
),
+ path(
+ "ajax/update/route53",
+ views.RegistrarSyncRoute53.as_view(),
+ name="ajax_update_route53",
+ ),
path(
"ajax/update/cloud",
views.MonitorCloudInfrastructure.as_view(),
diff --git a/ghostwriter/shepherd/views.py b/ghostwriter/shepherd/views.py
index 100df0c05..6c98b8764 100644
--- a/ghostwriter/shepherd/views.py
+++ b/ghostwriter/shepherd/views.py
@@ -27,6 +27,7 @@
from ghostwriter.commandcenter.models import (
CloudServicesConfiguration,
NamecheapConfiguration,
+ Route53Configuration,
VirusTotalConfiguration,
)
from ghostwriter.rolodex.models import Project
@@ -361,6 +362,37 @@ def post(self, request, *args, **kwargs):
return JsonResponse(data)
+class RegistrarSyncRoute53(LoginRequiredMixin, View):
+ """
+ Create an individual :model:`django_q.Task` under group ``Route53 Update`` with
+ :task:`shepherd.tasks.fetch_route53_domains` to create or update one or more
+ :model:`shepherd.Domain`.
+ """
+
+ def post(self, request, *args, **kwargs):
+ # Add an async task grouped as ``Route53 Update``
+ result = "success"
+ try:
+ task_id = async_task(
+ "ghostwriter.shepherd.tasks.fetch_route53_domains",
+ group="Route53 Update",
+ )
+ message = (
+ "Successfully queued Route53 update task (Task ID {task})".format(
+ task=task_id
+ )
+ )
+ except Exception:
+ result = "error"
+ message = "Route53 update task could not be queued"
+
+ data = {
+ "result": result,
+ "message": message,
+ }
+ return JsonResponse(data)
+
+
class MonitorCloudInfrastructure(LoginRequiredMixin, View):
"""
Create an individual :model:`django_q.Task` under group ``Cloud Infrastructure Review``
@@ -800,6 +832,16 @@ def update(request):
End time of latest :model:`django_q.Task` for group "Namecheap Update"
``namecheap_last_result``
Result of latest :model:`django_q.Task` for group "Namecheap Update"
+ ``enable_route53``
+ The associated value from :model:`commandcenter.Route53Configuration`
+ ``route53_last_update_requested``
+ Start time of latest :model:`django_q.Task` for group "Route53 Update"
+ ``route53_last_update_completed``
+ End time of latest :model:`django_q.Task` for group "Route53 Update"
+ ``route53_last_update_time``
+ End time of latest :model:`django_q.Task` for group "Route53 Update"
+ ``route53_last_result``
+ Result of latest :model:`django_q.Task` for group "Route53 Update"
``enable_cloud_monitor``
The associated value from :model:`commandcenter.CloudServicesConfiguration`
``cloud_last_update_requested``
@@ -824,6 +866,8 @@ def update(request):
enable_cloud_monitor = cloud_config.enable
namecheap_config = NamecheapConfiguration.get_solo()
enable_namecheap = namecheap_config.enable
+ route53_config = Route53Configuration.get_solo()
+ enable_route53 = route53_config.enable
# Collect data for category updates
cat_last_update_completed = ""
@@ -892,6 +936,25 @@ def update(request):
else:
namecheap_last_update_requested = "Namecheap Syncing is Disabled"
+ # Collect data for Route53 updates
+ route53_last_update_completed = ""
+ route53_last_update_time = ""
+ route53_last_result = ""
+ if enable_route53:
+ try:
+ queryset = Task.objects.filter(group="Route53 Update")[0]
+ route53_last_update_requested = queryset.started
+ route53_last_result = queryset.result
+ if queryset.success:
+ route53_last_update_completed = queryset.stopped
+ route53_last_update_time = round(queryset.time_taken() / 60, 2)
+ else:
+ route53_last_update_completed = "Failed"
+ except Exception:
+ route53_last_update_requested = "Route53 Sync Has Not Been Run Yet"
+ else:
+ route53_last_update_requested = "Route53 Syncing is Disabled"
+
# Collect data for cloud monitoring
cloud_last_update_completed = ""
cloud_last_update_time = ""
@@ -928,6 +991,11 @@ def update(request):
"namecheap_last_update_completed": namecheap_last_update_completed,
"namecheap_last_update_time": namecheap_last_update_time,
"namecheap_last_result": namecheap_last_result,
+ "enable_route53": enable_route53,
+ "route53_last_update_requested": route53_last_update_requested,
+ "route53_last_update_completed": route53_last_update_completed,
+ "route53_last_update_time": route53_last_update_time,
+ "route53_last_result": route53_last_result,
"enable_cloud_monitor": enable_cloud_monitor,
"cloud_last_update_requested": cloud_last_update_requested,
"cloud_last_update_completed": cloud_last_update_completed,