Skip to content

Commit

Permalink
Merge branch 'main' into fix/stop_yielding_network_in_certain_normali…
Browse files Browse the repository at this point in the history
…zers
  • Loading branch information
ammar92 authored Sep 3, 2024
2 parents af63d89 + 25e4d5c commit 0e70776
Show file tree
Hide file tree
Showing 15 changed files with 183 additions and 17 deletions.
Empty file.
9 changes: 9 additions & 0 deletions boefjes/boefjes/plugins/kat_dns_version/boefje.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "dns-bind-version",
"name": "DNS software version",
"description": "Uses the DNS VERSION.BIND command to attempt to learn the servers software.",
"consumes": [
"IPService"
],
"scan_level": 2
}
3 changes: 3 additions & 0 deletions boefjes/boefjes/plugins/kat_dns_version/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Fetch DNS Server software version

This boefje tries to detect the DNS Server version by doing a VERSION.BIND call.
42 changes: 42 additions & 0 deletions boefjes/boefjes/plugins/kat_dns_version/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Boefje script for getting namserver version"""

import json
from os import getenv

import dns
import dns.message
import dns.query

from boefjes.job_models import BoefjeMeta


def run(boefje_meta: BoefjeMeta) -> list[tuple[set, str | bytes]]:
input_ = boefje_meta.arguments["input"] # input is IPService
ip_port = input_["ip_port"]
if input_["service"]["name"] != "domain":
return [({"boefje/error"}, "Not a DNS service")]

ip = ip_port["address"]["address"]
port = int(ip_port["port"])
protocol = ip_port["protocol"]

timeout = float(getenv("TIMEOUT", 30))

method = dns.query.udp if protocol == "udp" else dns.query.tcp

queries = [
dns.message.make_query("VERSION.BIND", dns.rdatatype.TXT, dns.rdataclass.CHAOS),
dns.message.make_query("VERSION.SERVER", dns.rdatatype.TXT, dns.rdataclass.CHAOS),
]

results = []
for query in queries:
response = method(query, where=ip, timeout=timeout, port=port)

try:
answer = response.answer[0]
results.append(answer.to_rdataset().pop().strings[0].decode())
except IndexError:
pass

return [(set(), json.dumps(results))]
36 changes: 36 additions & 0 deletions boefjes/boefjes/plugins/kat_dns_version/normalize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import json
from collections.abc import Iterable

from boefjes.job_models import NormalizerOutput
from octopoes.models import Reference
from octopoes.models.ooi.software import Software, SoftwareInstance


def run(input_ooi: dict, raw: bytes) -> Iterable[NormalizerOutput]:
input_ooi_reference = Reference.from_str(input_ooi["primary_key"])

results = json.loads(raw)
for version in results:
if version.startswith("bind"):
name = "bind"
version_number = version.split("-")[1]
elif version.startswith("9."):
name = "bind"
version_number = version
elif version.startswith("Microsoft DNS"):
name = "Microsoft DNS"
version_number = version.replace("Microsoft DNS ", "").split(" ")[0]
elif version.startswith("dnsmasq"):
name = "dnsmasq"
version_number = version.split("-")[1]
elif version.startswith("PowerDNS"):
name = "PowerDNS"
version_number = version.replace("PowerDNS Authoritative Server ", "").split(" ")[0]
else:
name = None
version_number = None

if name and version_number:
software = Software(name=name, version=version_number)
software_instance = SoftwareInstance(ooi=input_ooi_reference, software=software.reference)
yield from [software, software_instance]
10 changes: 10 additions & 0 deletions boefjes/boefjes/plugins/kat_dns_version/normalizer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "dns-bind-version-normalize",
"consumes": [
"boefje/dns-bind-version"
],
"produces": [
"Software",
"SoftwareInstance"
]
}
13 changes: 13 additions & 0 deletions boefjes/boefjes/plugins/kat_dns_version/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"title": "Arguments",
"type": "object",
"properties": {
"TIMEOUT": {
"title": "TIMEOUT",
"type": "integer",
"description": "Timeout for requests to the targeted dns servers",
"default": 30,
"minimum": 0
}
}
}
8 changes: 4 additions & 4 deletions boefjes/tests/integration/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ def test_get_local_plugin(self):

def test_filter_plugins(self):
response = self.client.get(f"/v1/organisations/{self.org.id}/plugins/")
self.assertEqual(len(response.json()), 97)
self.assertEqual(len(response.json()), 99)
response = self.client.get(f"/v1/organisations/{self.org.id}/plugins?plugin_type=boefje")
self.assertEqual(len(response.json()), 43)
self.assertEqual(len(response.json()), 44)

response = self.client.get(f"/v1/organisations/{self.org.id}/plugins?limit=10")
self.assertEqual(len(response.json()), 10)
Expand All @@ -74,7 +74,7 @@ def test_add_boefje(self):
self.assertEqual(response.status_code, 422)

response = self.client.get(f"/v1/organisations/{self.org.id}/plugins/?plugin_type=boefje")
self.assertEqual(len(response.json()), 44)
self.assertEqual(len(response.json()), 45)

boefje_dict = boefje.dict()
boefje_dict["consumes"] = list(boefje_dict["consumes"])
Expand All @@ -99,7 +99,7 @@ def test_add_normalizer(self):
self.assertEqual(response.status_code, 201)

response = self.client.get(f"/v1/organisations/{self.org.id}/plugins/?plugin_type=normalizer")
self.assertEqual(len(response.json()), 55)
self.assertEqual(len(response.json()), 56)

response = self.client.get(f"/v1/organisations/{self.org.id}/plugins/test_normalizer")
self.assertEqual(response.json(), normalizer.dict())
Expand Down
2 changes: 1 addition & 1 deletion rocky/reports/templates/partials/return_button.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{% csrf_token %}
{% include "forms/report_form_fields.html" %}

<button type="submit" class="button ghost">
<button name="return" type="submit" class="button ghost">
<span class="icon ti-chevron-left"></span>
{% blocktranslate %}{{ btn_text }}{% endblocktranslate %}
</button>
Expand Down
16 changes: 14 additions & 2 deletions rocky/reports/views/aggregate_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from tools.view_helpers import PostRedirect

from reports.report_types.aggregate_organisation_report.report import AggregateOrganisationReport
from reports.report_types.definitions import AggregateReport, MultiReport, Report
Expand Down Expand Up @@ -118,7 +119,7 @@ def setup(self, request, *args, **kwargs):
def post(self, request, *args, **kwargs):
if not self.selected_oois:
messages.error(request, self.NONE_OOI_SELECTION_MESSAGE)
return redirect(self.get_previous())
return PostRedirect(self.get_previous())
return self.get(request, *args, **kwargs)

def get_report_types_for_aggregate_report(
Expand Down Expand Up @@ -151,9 +152,17 @@ class SetupScanAggregateReportView(
current_step = 3

def post(self, request, *args, **kwargs):
# If the user wants to change selection, but all plugins are enabled, it needs to go even further back
if not self.selected_report_types:
messages.error(request, self.NONE_REPORT_TYPE_SELECTION_MESSAGE)
return redirect(self.get_previous())
return PostRedirect(self.get_previous())

if "return" in self.request.POST and self.plugins_enabled():
return PostRedirect(self.get_previous())

if self.plugins_enabled():
return PostRedirect(self.get_next())

return self.get(request, *args, **kwargs)


Expand All @@ -167,6 +176,9 @@ class ExportSetupAggregateReportView(AggregateReportStepsMixin, BreadcrumbsAggre
current_step = 4

def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
if not self.selected_report_types:
messages.error(request, self.NONE_REPORT_TYPE_SELECTION_MESSAGE)
return PostRedirect(self.get_previous())
return super().get(request, *args, **kwargs)

def get_context_data(self, **kwargs):
Expand Down
14 changes: 12 additions & 2 deletions rocky/reports/views/generate_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from tools.view_helpers import PostRedirect

from octopoes.models import Reference
from reports.report_types.helpers import get_ooi_types_with_report, get_report_types_for_oois
Expand Down Expand Up @@ -108,7 +109,7 @@ class ReportTypesSelectionGenerateReportView(
def post(self, request, *args, **kwargs):
if not self.selected_oois:
messages.error(request, self.NONE_OOI_SELECTION_MESSAGE)
return redirect(self.get_previous())
return PostRedirect(self.get_previous())
return self.get(request, *args, **kwargs)

def get_context_data(self, **kwargs):
Expand All @@ -135,7 +136,13 @@ class SetupScanGenerateReportView(
def post(self, request, *args, **kwargs):
if not self.selected_report_types:
messages.error(request, self.NONE_REPORT_TYPE_SELECTION_MESSAGE)
return redirect(self.get_previous())
return PostRedirect(self.get_previous())

if "return" in self.request.POST and self.plugins_enabled():
return PostRedirect(self.get_previous())

if self.plugins_enabled():
return PostRedirect(self.get_next())
return self.get(request, *args, **kwargs)


Expand All @@ -150,6 +157,9 @@ class ExportSetupGenerateReportView(GenerateReportStepsMixin, BreadcrumbsGenerat
reports: dict[str, str] = {}

def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
if not self.selected_report_types:
messages.error(request, self.NONE_REPORT_TYPE_SELECTION_MESSAGE)
return PostRedirect(self.get_previous())
self.reports = create_report_names(self.oois_pk, self.report_types)
return super().get(request, *args, **kwargs)

Expand Down
21 changes: 21 additions & 0 deletions rocky/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1785,3 +1785,24 @@ def boefje_dns_records():
runnable_hash=None,
produces={"boefje/dns-records"},
)


@pytest.fixture
def boefje_nmap_tcp():
return Boefje(
id="nmap",
name="Nmap TCP",
version=None,
authors=None,
created=None,
description="Defaults to top 250 TCP ports. Includes service detection.",
environment_keys=None,
related=[],
enabled=True,
type="boefje",
scan_level=SCAN_LEVEL.L2,
consumes={IPAddressV4, IPAddressV6},
options=None,
runnable_hash=None,
produces={"boefje/nmap"},
)
12 changes: 7 additions & 5 deletions rocky/tests/reports/test_aggregate_report_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def test_report_types_selection_nothing_selected(

response = SetupScanAggregateReportView.as_view()(request, organization_code=client_member.organization.code)

assert response.status_code == 302
assert response.status_code == 307
assert list(request._messages)[0].message == "Select at least one report type to proceed."


Expand All @@ -189,6 +189,7 @@ def test_report_types_selection(
listed_hostnames,
mocker,
boefje_dns_records,
boefje_nmap_tcp,
rocky_health,
mock_bytes_client,
):
Expand All @@ -197,7 +198,7 @@ def test_report_types_selection(
"""

katalogus_mocker = mocker.patch("reports.views.base.get_katalogus")()
katalogus_mocker.get_plugins.return_value = [boefje_dns_records]
katalogus_mocker.get_plugins.return_value = [boefje_dns_records, boefje_nmap_tcp]

rocky_health_mocker = mocker.patch("reports.report_types.aggregate_organisation_report.report.get_rocky_health")()
rocky_health_mocker.return_value = rocky_health
Expand All @@ -211,16 +212,17 @@ def test_report_types_selection(
request = setup_request(
rf.post(
"aggregate_report_setup_scan",
{"observed_at": valid_time.strftime("%Y-%m-%d"), "report_type": "dns-report"},
{"observed_at": valid_time.strftime("%Y-%m-%d"), "report_type": ["dns-report", "systems-report"]},
),
client_member.user,
)

response = SetupScanAggregateReportView.as_view()(request, organization_code=client_member.organization.code)

assert response.status_code == 200 # if all plugins are enabled the view will auto redirect to generate report
assert response.status_code == 307 # if all plugins are enabled the view will auto redirect to generate report

assertContains(response, '<input type="hidden" name="report_type" value="dns-report">', html=True)
# Redirect to export setup
assert response.headers["Location"] == "/en/test/reports/aggregate-report/export-setup/?"


def test_save_aggregate_report_view(
Expand Down
9 changes: 6 additions & 3 deletions rocky/tests/reports/test_generate_report_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ def test_report_types_selection_nothing_selected(

response = SetupScanGenerateReportView.as_view()(request, organization_code=client_member.organization.code)

assert response.status_code == 302
assert response.status_code == 307

assert list(request._messages)[0].message == "Select at least one report type to proceed."


Expand Down Expand Up @@ -214,8 +215,10 @@ def test_report_types_selection(

response = SetupScanGenerateReportView.as_view()(request, organization_code=client_member.organization.code)

assert response.status_code == 200
assertContains(response, '<input type="hidden" name="report_type" value="dns-report">', html=True)
assert response.status_code == 307

# Redirect to export setup, all plugins are then enabled
assert response.headers["Location"] == "/en/test/reports/generate-report/export-setup/?"


def test_save_generate_report_view(
Expand Down
5 changes: 5 additions & 0 deletions rocky/tools/view_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from urllib.parse import urlencode, urlparse, urlunparse

from django.http import HttpRequest
from django.http.response import HttpResponseRedirectBase
from django.urls.base import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -163,3 +164,7 @@ def build_breadcrumbs(self):
"text": _("Objects"),
}
]


class PostRedirect(HttpResponseRedirectBase):
status_code = 307

0 comments on commit 0e70776

Please sign in to comment.