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 9 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 @@ -404,6 +404,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_ALERT_IN_DAYS", default=5, cast=int)
kazet marked this conversation as resolved.
Show resolved Hide resolved

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

from karton.core import Task
from whois import 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] = {}
if is_main_domain(domain):
now = datetime.datetime.now()
domain_data = query(domain)
kazet marked this conversation as resolved.
Show resolved Hide resolved
expiry_date = domain_data.expiration_date
days_to_expire = None
if expiry_date:
days_to_expire = (expiry_date - now).days
result["expiration_date"] = expiry_date
if (
days_to_expire
and days_to_expire <= Config.Modules.DomainExpirationScanner.DOMAIN_EXPIRATION_TIMEFRAME_DAYS
kazet marked this conversation as resolved.
Show resolved Hide resolved
):
result["close_expiry_date"] = True
result["days_to_expire"] = days_to_expire
status = TaskStatus.INTERESTING
kazet marked this conversation as resolved.
Show resolved Hide resolved
status_reason = (
f"Scanned domain will expire in {days_to_expire} days - {expiry_date}."
kazet marked this conversation as resolved.
Show resolved Hide resolved
if days_to_expire != 1
else f"Scanned domain will expire in {days_to_expire} day - (on {expiry_date})."
)
else:
status = TaskStatus.OK
kazet marked this conversation as resolved.
Show resolved Hide resolved
status_reason = None
self.db.save_task_result(task=current_task, status=status, status_reason=status_reason, data=result)
else:
status = TaskStatus.OK
status_reason = None
self.db.save_task_result(task=current_task, status=status, status_reason=status_reason, data=result)


if __name__ == "__main__":
DomainExpirationScanner().loop()
74 changes: 74 additions & 0 deletions artemis/reporting/modules/domain_expiration_scanner/reporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import os
from typing import Any, Callable, Dict, List

from artemis.reporting.base.language import Language
from artemis.reporting.base.normal_form import (
NormalForm,
get_domain_normal_form,
get_domain_score,
)
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,
),
]

@staticmethod
def get_scoring_rules() -> Dict[ReportType, Callable[[Report], List[int]]]:
"""See the docstring in the parent class."""
return {
DomainExpirationScannerReporter.CLOSE_DOMAIN_EXPIRATION_DATE: lambda report: [
get_domain_score(report.target)
]
}

@staticmethod
def get_normal_form_rules() -> Dict[ReportType, Callable[[Report], NormalForm]]:
"""See the docstring in the Reporter class."""
return {
DomainExpirationScannerReporter.CLOSE_DOMAIN_EXPIRATION_DATE: lambda report: Reporter.dict_to_tuple(
{
"type": report.report_type,
"target": get_domain_normal_form(report.target),
"message": report.additional_data["expiration_date"],
}
)
}
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 @@ -335,6 +335,16 @@ services:
restart: always
volumes: ["./docker/karton.ini:/etc/karton/karton.ini"]

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.16
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}}
)