Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report that the domain will soon expire #495 #544

Merged
merged 23 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6c92a3b
Check that the domain will soon expire, (#495)
anna1492 Sep 22, 2023
eb1182d
Report that the domain will soon expire - reporter, scanner-changes (…
anna1492 Sep 26, 2023
82af5b9
Report that the domain will soon expire - translation, (#495)
anna1492 Oct 2, 2023
3128538
Report that the domain will soon expire - translation - update, (#495)
anna1492 Oct 2, 2023
074d4cd
Report that the domain will soon expire - tests, (#495)
anna1492 Oct 3, 2023
cc85f34
Report that the domain will soon expire - changes, improvements (#495)
anna1492 Oct 4, 2023
1639719
Report that the domain will soon expire - translation - update (#495)
anna1492 Oct 4, 2023
8bc1abf
Report that the domain will soon expire - changes (#495)
anna1492 Oct 10, 2023
5c0093c
Report that the domain will soon expire - additional changes (#495)
anna1492 Oct 11, 2023
a1ca9fc
Merge branch 'main' into domain-expiration-scanner
kazet Oct 11, 2023
a1eb893
Supporting domains in normal forms
kazet Oct 11, 2023
3062654
lint
kazet Oct 11, 2023
6804fd9
lint
kazet Oct 11, 2023
64155d0
Report that the domain will soon expire - config - change (#495)
anna1492 Oct 12, 2023
22ef702
Merge branch 'domain-expiration-scanner' of github.com:CERT-Polska/Ar…
anna1492 Oct 13, 2023
46180b1
Report that the domain will soon expire - whois limit (#495)
anna1492 Oct 17, 2023
6e429b2
Report that the domain will soon expire - log message - change (#495)
anna1492 Oct 18, 2023
93aa8d3
Report that the domain will soon expire - change (#495)
anna1492 Oct 18, 2023
c74c922
Merge branch 'main' into domain-expiration-scanner
anna1492 Oct 18, 2023
6e942a8
Report that the domain will soon expire - rate limit, refactoring (#495)
anna1492 Oct 19, 2023
fbc2a1c
Report that the domain will soon expire - rate limit - change, refact…
anna1492 Oct 23, 2023
6d3f81c
Report that the domain will soon expire - fix (#495)
anna1492 Oct 23, 2023
ce5f5b3
Report that the domain will soon expire - additional type hints (#495)
anna1492 Oct 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions artemis/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,11 @@ class WordPressBruter:
"www123, when testing www.projectname.example.com.",
] = get_config("WORDPRESS_BRUTER_STRIPPED_PREFIXES", default="www", cast=decouple.Csv(str))

class DomainExpirationScanner:
DOMAIN_EXPIRATION_TIMEFRAME_DAYS: Annotated[
int, "The scanner warns if the domain's expiration date falls within this time frame from now."
] = get_config("DOMAIN_EXPIRATION_TIMEFRAME_DAYS", default=5, cast=int)

@staticmethod
def verify_each_variable_is_annotated() -> None:
def verify_class(cls: type) -> None:
Expand Down
74 changes: 74 additions & 0 deletions artemis/modules/domain_expiration_scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
import datetime
import time
from typing import Any, Dict, Optional

from karton.core import Task
from whois import Domain, WhoisQuotaExceeded, query # type: ignore

from artemis.binds import TaskStatus, TaskType
from artemis.config import Config
from artemis.domains import is_main_domain
from artemis.module_base import ArtemisBase


class DomainExpirationScanner(ArtemisBase):
"""
Alerts if domain expiration date is coming.
"""

identity = "domain_expiration_scanner"
filters = [{"type": TaskType.DOMAIN.value}]

def run(self, current_task: Task) -> None:
domain = current_task.get_payload(TaskType.DOMAIN)
result: Dict[str, Any] = {}
status = TaskStatus.OK
status_reason = None
if is_main_domain(domain):
try:
domain_data = self._query_whois(domain=domain)
except WhoisQuotaExceeded:
time.sleep(24 * 60 * 60)
kazet marked this conversation as resolved.
Show resolved Hide resolved
domain_data = self._query_whois(domain=domain)
kazet marked this conversation as resolved.
Show resolved Hide resolved

expiry_date = domain_data.expiration_date
result = self._prepare_expiration_data(expiration_date=expiry_date, result=result)

if "close_expiration_date" in result:
status = TaskStatus.INTERESTING
status_reason = self._prepare_expiration_status_reason(
days_to_expire=result["days_to_expire"], expiration_date=result["expiration_date"]
)

self.db.save_task_result(task=current_task, status=status, status_reason=status_reason, data=result)

@staticmethod
def _query_whois(domain: str) -> Domain:
return query(domain)

@staticmethod
def _prepare_expiration_data(
expiration_date: Optional[datetime.datetime], result: Dict[str, Any]
) -> Dict[str, Any]:
days_to_expire = None
now = datetime.datetime.now()
if expiration_date:
days_to_expire = (expiration_date - now).days
result["expiration_date"] = expiration_date
if days_to_expire and days_to_expire <= Config.Modules.DomainExpirationScanner.DOMAIN_EXPIRATION_TIMEFRAME_DAYS:
result["close_expiration_date"] = True
result["days_to_expire"] = days_to_expire
return result

@staticmethod
def _prepare_expiration_status_reason(days_to_expire: int, expiration_date: datetime.datetime) -> str:
return (
f"Scanned domain will expire in {days_to_expire} days - (on {expiration_date})."
if days_to_expire != 1
else f"Scanned domain will expire in {days_to_expire} day - (on {expiration_date})."
)


if __name__ == "__main__":
DomainExpirationScanner().loop()
16 changes: 13 additions & 3 deletions artemis/reporting/base/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
from typing import Any, Callable, Dict, List, Tuple

from .language import Language
from .normal_form import NormalForm, get_url_normal_form, get_url_score
from .normal_form import (
NormalForm,
get_domain_normal_form,
get_domain_score,
get_url_normal_form,
get_url_score,
)
from .report import Report
from .report_type import ReportType
from .templating import ReportEmailTemplateFragment
Expand Down Expand Up @@ -64,13 +70,17 @@ def dict_to_tuple(d: Dict[str, str]) -> Tuple[Tuple[str, str], ...]:

@staticmethod
def default_scoring_rule(report: Report) -> List[int]:
return [get_url_score(report.target)]
assert report.target_is_url() or report.target_is_domain()
return [get_url_score(report.target) if report.target_is_url() else get_domain_score(report.target)]

@staticmethod
def default_normal_form_rule(report: Report) -> NormalForm:
assert report.target_is_url() or report.target_is_domain()
return Reporter.dict_to_tuple(
{
"type": report.report_type,
"target": get_url_normal_form(report.target),
"target": get_url_normal_form(report.target)
if report.target_is_url()
else get_domain_normal_form(report.target),
}
)
47 changes: 47 additions & 0 deletions artemis/reporting/modules/domain_expiration_scanner/reporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import os
from typing import Any, Dict, List

from artemis.reporting.base.language import Language
from artemis.reporting.base.report import Report
from artemis.reporting.base.report_type import ReportType
from artemis.reporting.base.reporter import Reporter
from artemis.reporting.base.templating import ReportEmailTemplateFragment
from artemis.reporting.utils import get_top_level_target


class DomainExpirationScannerReporter(Reporter):
CLOSE_DOMAIN_EXPIRATION_DATE = ReportType("close_domain_expiration_date")

@staticmethod
def create_reports(task_result: Dict[str, Any], language: Language) -> List[Report]:
if task_result["headers"]["receiver"] != "domain_expiration_scanner":
return []

if not task_result["status"] == "INTERESTING":
return []

if not isinstance(task_result["result"], dict):
return []

data = task_result["result"]
expiration_date_from_result = data["expiration_date"]
expiration_date = expiration_date_from_result.strftime("%d-%m-%Y")

return [
Report(
top_level_target=get_top_level_target(task_result),
target=task_result["payload"]["domain"],
report_type=DomainExpirationScannerReporter.CLOSE_DOMAIN_EXPIRATION_DATE,
additional_data={"expiration_date": expiration_date},
timestamp=task_result["created_at"],
)
]

@staticmethod
def get_email_template_fragments() -> List[ReportEmailTemplateFragment]:
return [
ReportEmailTemplateFragment.from_file(
os.path.join(os.path.dirname(__file__), "template_close_domain_expiration_scanner.jinja2"),
priority=5,
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% if "close_domain_expiration_date" in data.contains_type %}
<li>{% trans %}The following domains will soon expire: {% endtrans %}
<ul>
{% for report in data.reports %}
{% if report.report_type == "close_domain_expiration_date" %}
<li>
<p> {{ report.target }} - {% trans %}will expire on{% endtrans %} {{ report.additional_data["expiration_date"] }} </p>
</li>
{% endif %}
{% endfor %}
</ul>
</li>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#: artemis/reporting/modules/domain_expiration_scanner/template_close_domain_expiration_scanner.jinja2:2
msgid "The following domains will soon expire: "
msgstr ""

#: artemis/reporting/modules/domain_expiration_scanner/template_close_domain_expiration_scanner.jinja2:7
msgid "will expire on"
msgstr ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#: artemis/reporting/modules/domain_expiration_scanner/template_close_domain_expiration_scanner.jinja2:2
msgid "The following domains will soon expire: "
msgstr "Poniższe domeny wkrótce wygasną: "

#: artemis/reporting/modules/domain_expiration_scanner/template_close_domain_expiration_scanner.jinja2:7
msgid "will expire on"
msgstr "wygaśnie dnia"
1 change: 1 addition & 0 deletions artemis/reporting/severity.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Severity(str, Enum):
ReportType("directory_index"): Severity.MEDIUM,
ReportType("open_port_remote_desktop"): Severity.MEDIUM,
ReportType("exposed_bash_history"): Severity.MEDIUM,
ReportType("close_domain_expiry_date"): Severity.MEDIUM,
ReportType("certificate_authority_invalid"): Severity.LOW,
ReportType("expired_ssl_certificate"): Severity.LOW,
ReportType("almost_expired_ssl_certificate"): Severity.LOW,
Expand Down
10 changes: 10 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,16 @@ services:
restart: always
volumes: ["./docker/karton.ini:/etc/karton/karton.ini", "${DOCKER_COMPOSE_ADDITIONAL_SHARED_DIRECTORY:-./shared}:/shared/"]

karton-domain_expiration_scanner:
build:
context: .
dockerfile: docker/Dockerfile
command: "python3 -m artemis.modules.domain_expiration_scanner"
depends_on: [ karton-logger ]
env_file: .env
restart: always
volumes: [ "./docker/karton.ini:/etc/karton/karton.ini" ]

volumes:
data-mongodb:
data-redis:
Expand Down
2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM python:3.11-alpine3.18
COPY docker/wait-for-it.sh /wait-for-it.sh

ARG ADDITIONAL_REQUIREMENTS
RUN apk add --no-cache --virtual .build-deps go gcc git libc-dev make libffi-dev libpcap-dev postgresql-dev && \
RUN apk add --no-cache --virtual .build-deps go gcc git libc-dev make libffi-dev libpcap-dev postgresql-dev whois && \
apk add --no-cache bash libpcap libpq git subversion
RUN GOBIN=/usr/local/bin/ go install github.com/projectdiscovery/naabu/v2/cmd/[email protected] && \
GOBIN=/usr/local/bin/ go install github.com/praetorian-inc/fingerprintx/cmd/[email protected] && \
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ typing-extensions==4.8.0
urllib3==1.26.17
uvicorn==0.23.2
validators==0.22.0
whois==0.9.27
28 changes: 28 additions & 0 deletions test/modules/test_domain_expiration_scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import datetime
from test.base import ArtemisModuleTestCase
from unittest.mock import patch

from karton.core import Task

from artemis.binds import TaskStatus, TaskType
from artemis.modules.domain_expiration_scanner import DomainExpirationScanner


class TestDomainExpirationScanner(ArtemisModuleTestCase):
karton_class = DomainExpirationScanner # type: ignore

def test_simple(self) -> None:
task = Task(
{"type": TaskType.DOMAIN.value},
payload={"domain": "google.com"},
)

with patch("artemis.config.Config.Modules.DomainExpirationScanner") as mocked_config:
mocked_config.DOMAIN_EXPIRATION_TIMEFRAME_DAYS = 5000
self.run_task(task)

(call,) = self.mock_db.save_task_result.call_args_list
self.assertEqual(call.kwargs["status"], TaskStatus.INTERESTING)
reason = call.kwargs["status_reason"]
self.assertTrue(reason.startswith("Scanned domain will expire in"))
self.assertTrue(isinstance(call.kwargs["data"]["expiration_date"], datetime.datetime))
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from test.base import BaseReportingTest
from unittest.mock import patch

from karton.core import Task

from artemis.binds import TaskType
from artemis.modules.domain_expiration_scanner import DomainExpirationScanner
from artemis.reporting.base.language import Language
from artemis.reporting.base.reporters import reports_from_task_result


class DomainExpirationScannerAutoreporterIntegrationTest(BaseReportingTest):
karton_class = DomainExpirationScanner # type: ignore

def test_domain_expiration(self) -> None:
message = self._run_task_and_get_message("google.com")
self.assertIn("The following domains will soon expire:", message)
self.assertIn("google.com", message)

def _run_task_and_get_message(self, domain: str) -> str:
task = Task(
{"type": TaskType.DOMAIN},
payload={TaskType.DOMAIN: domain},
)
with patch("artemis.config.Config.Modules.DomainExpirationScanner") as mocked_config:
mocked_config.DOMAIN_EXPIRATION_TIMEFRAME_DAYS = 5000
self.run_task(task)
(call,) = self.mock_db.save_task_result.call_args_list
data = {
"created_at": None,
"headers": {
"receiver": "domain_expiration_scanner",
},
"payload": {
"domain": domain,
},
"payload_persistent": {
"original_domain": domain,
},
"status": "INTERESTING",
"result": call.kwargs["data"],
}

reports = reports_from_task_result(data, Language.en_US)
message_template = self.generate_message_template()
return message_template.render(
{"data": {"contains_type": set([report.report_type for report in reports]), "reports": reports}}
)
Loading