diff --git a/rocky/rocky/locale/django.pot b/rocky/rocky/locale/django.pot index 67ccc529f3c..8d7603aad8a 100644 --- a/rocky/rocky/locale/django.pot +++ b/rocky/rocky/locale/django.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-09-04 14:21+0000\n" +"POT-Creation-Date: 2024-09-05 08:44+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -5271,6 +5271,10 @@ msgstr "" msgid "When" msgstr "" +#: rocky/templates/oois/ooi_detail_origins_observations.html +msgid "This scan was manually created." +msgstr "" + #: rocky/templates/oois/ooi_detail_origins_observations.html msgid "The boefje has since been deleted or disabled." msgstr "" diff --git a/rocky/rocky/templates/oois/ooi_detail_origins_observations.html b/rocky/rocky/templates/oois/ooi_detail_origins_observations.html index c4b4e60cc05..e60225d97d6 100644 --- a/rocky/rocky/templates/oois/ooi_detail_origins_observations.html +++ b/rocky/rocky/templates/oois/ooi_detail_origins_observations.html @@ -24,7 +24,11 @@

{% translate "Last observed by" %}

{{ observation.boefje.name }} {% else %} - {% translate "The boefje has since been deleted or disabled." %} + {% if observation.normalizer.raw_data.boefje_meta.boefje.id == "manual" %} + {% translate "This scan was manually created." %} + {% else %} + {% translate "The boefje has since been deleted or disabled." %} + {% endif %} {% endif %} diff --git a/rocky/rocky/templates/partials/ooi_detail_related_object.html b/rocky/rocky/templates/partials/ooi_detail_related_object.html index 30ebfcb5467..f70076a4c1e 100644 --- a/rocky/rocky/templates/partials/ooi_detail_related_object.html +++ b/rocky/rocky/templates/partials/ooi_detail_related_object.html @@ -12,7 +12,7 @@

{% translate "Related objects" %}

{% if not ooi_past_due %} {% if not ooi|is_finding and not ooi|is_finding_type %} {% endif %} diff --git a/rocky/rocky/views/mixins.py b/rocky/rocky/views/mixins.py index d8bc37268ed..116ae4cd234 100644 --- a/rocky/rocky/views/mixins.py +++ b/rocky/rocky/views/mixins.py @@ -172,16 +172,20 @@ def get_origins( boefje_meta = normalizer_data["raw_data"]["boefje_meta"] boefje_id = boefje_meta["boefje"]["id"] if boefje_meta.get("ended_at"): - boefje_meta["ended_at"] = datetime.strptime(boefje_meta["ended_at"], "%Y-%m-%dT%H:%M:%S.%fZ") + try: + boefje_meta["ended_at"] = datetime.strptime(boefje_meta["ended_at"], "%Y-%m-%dT%H:%M:%S.%fZ") + except ValueError: + boefje_meta["ended_at"] = datetime.strptime(boefje_meta["ended_at"], "%Y-%m-%dT%H:%M:%SZ") origin.normalizer = normalizer_data - try: - origin.boefje = katalogus.get_plugin(boefje_id) - except HTTPError as e: - logger.error( - "Could not load boefje: %s from katalogus, error: %s", - boefje_id, - e, - ) + if boefje_id != "manual": + try: + origin.boefje = katalogus.get_plugin(boefje_id) + except HTTPError as e: + logger.error( + "Could not load boefje %s from katalogus: %s", + boefje_id, + e, + ) observations.append(origin) return results diff --git a/rocky/rocky/views/ooi_detail.py b/rocky/rocky/views/ooi_detail.py index fb086b97d28..16b610a4508 100644 --- a/rocky/rocky/views/ooi_detail.py +++ b/rocky/rocky/views/ooi_detail.py @@ -13,14 +13,14 @@ from octopoes.models import Reference from octopoes.models.ooi.question import Question -from rocky.views.ooi_detail_related_object import OOIFindingManager, OOIRelatedObjectAddView +from rocky.views.ooi_detail_related_object import OOIFindingManager, OOIRelatedObjectManager from rocky.views.ooi_view import BaseOOIDetailView from rocky.views.tasks import TaskListView class OOIDetailView( BaseOOIDetailView, - OOIRelatedObjectAddView, + OOIRelatedObjectManager, OOIFindingManager, TaskListView, ): diff --git a/rocky/rocky/views/ooi_detail_related_object.py b/rocky/rocky/views/ooi_detail_related_object.py index 356c189b9c4..3840f235246 100644 --- a/rocky/rocky/views/ooi_detail_related_object.py +++ b/rocky/rocky/views/ooi_detail_related_object.py @@ -3,6 +3,7 @@ from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from django.views.generic.base import TemplateView from tools.ooi_helpers import format_attr_name from tools.view_helpers import existing_ooi_type, get_mandatory_fields, url_with_querystring @@ -29,54 +30,6 @@ def get_related_objects(self, observed_at): related.append(rel) return related - -class OOIFindingManager(SingleOOITreeMixin): - def get_findings(self) -> list[Finding]: - findings = [] - for relation in self.tree.root.children.values(): - for child in relation: - ooi = self.tree.store[str(child.reference)] - if isinstance(ooi, Finding) and ooi.reference != self.tree.root.reference: - findings.append(ooi) - return findings - - def count_findings_per_severity(self) -> Counter: - counter = Counter({severity: 0 for severity in RiskLevelSeverity}) - for finding in self.get_findings(): - finding_type: FindingType | None = self.tree.store.get(str(finding.finding_type), None) - if finding_type is not None and finding_type.risk_severity is not None: - counter.update([finding_type.risk_severity]) - else: - counter.update([RiskLevelSeverity.UNKNOWN]) - return counter - - def get_finding_details_sorted_by_score_desc(self) -> list[tuple[Finding, FindingType]]: - finding_details = self.get_finding_details() - return list(sorted(finding_details, key=lambda x: x[1].risk_score or 0, reverse=True)) - - def get_finding_details(self) -> list[tuple[Finding, FindingType]]: - return [(finding, self.tree.store[str(finding.finding_type)]) for finding in self.get_findings()] - - -class OOIRelatedObjectAddView(OOIRelatedObjectManager): - template_name = "oois/ooi_detail_add_related_object.html" - - def get(self, request, *args, **kwargs): - if "ooi_id" in request.GET: - self.ooi_id = self.get_ooi(pk=request.GET.get("ooi_id")) - - if "add_ooi_type" in request.GET: - ooi_type_choice = self.split_ooi_type_choice(request.GET["add_ooi_type"]) - if existing_ooi_type(ooi_type_choice["ooi_type"]): - return redirect(self.ooi_add_url(self.ooi_id, **ooi_type_choice)) - - if "status_code" in kwargs: - response = super().get(request, *args, **kwargs) - response.status_code = kwargs["status_code"] - return response - - return super().get(request, *args, **kwargs) - def split_ooi_type_choice(self, ooi_type_choice) -> dict[str, str]: ooi_type = ooi_type_choice.split("|", 1) @@ -146,6 +99,54 @@ def get_ooi_types_input_values(self, ooi: OOI) -> list[dict[str, str]]: return input_values + +class OOIFindingManager(SingleOOITreeMixin): + def get_findings(self) -> list[Finding]: + findings = [] + for relation in self.tree.root.children.values(): + for child in relation: + ooi = self.tree.store[str(child.reference)] + if isinstance(ooi, Finding) and ooi.reference != self.tree.root.reference: + findings.append(ooi) + return findings + + def count_findings_per_severity(self) -> Counter: + counter = Counter({severity: 0 for severity in RiskLevelSeverity}) + for finding in self.get_findings(): + finding_type: FindingType | None = self.tree.store.get(str(finding.finding_type), None) + if finding_type is not None and finding_type.risk_severity is not None: + counter.update([finding_type.risk_severity]) + else: + counter.update([RiskLevelSeverity.UNKNOWN]) + return counter + + def get_finding_details_sorted_by_score_desc(self) -> list[tuple[Finding, FindingType]]: + finding_details = self.get_finding_details() + return list(sorted(finding_details, key=lambda x: x[1].risk_score or 0, reverse=True)) + + def get_finding_details(self) -> list[tuple[Finding, FindingType]]: + return [(finding, self.tree.store[str(finding.finding_type)]) for finding in self.get_findings()] + + +class OOIRelatedObjectAddView(OOIRelatedObjectManager, TemplateView): + template_name = "oois/ooi_detail_add_related_object.html" + + def get(self, request, *args, **kwargs): + if "ooi_id" in request.GET: + self.ooi_id = self.get_ooi(pk=request.GET["ooi_id"]) + + if "add_ooi_type" in request.GET: + ooi_type_choice = self.split_ooi_type_choice(request.GET["add_ooi_type"]) + if existing_ooi_type(ooi_type_choice["ooi_type"]): + return redirect(self.ooi_add_url(self.ooi_id, **ooi_type_choice)) + + if "status_code" in kwargs: + response = super().get(request, *args, **kwargs) + response.status_code = kwargs["status_code"] + return response + + return super().get(request, *args, **kwargs) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["ooi_id"] = self.ooi_id diff --git a/rocky/tests/objects/test_objects_detail.py b/rocky/tests/objects/test_objects_detail.py index 71d5d4d63a1..4bd7b7653e1 100644 --- a/rocky/tests/objects/test_objects_detail.py +++ b/rocky/tests/objects/test_objects_detail.py @@ -71,7 +71,7 @@ def test_ooi_detail( response = OOIDetailView.as_view()(request, organization_code=client_member.organization.code) assert response.status_code == 200 - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 assertContains(response, "Object") assertContains(response, "Network|testnetwork") @@ -109,7 +109,7 @@ def test_question_detail( response = OOIDetailView.as_view()(request, organization_code=client_member.organization.code) assert response.status_code == 200 - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 assertContains(response, "Question") assertContains(response, "Rendered Question Form") @@ -142,7 +142,7 @@ def test_answer_question( response = OOIDetailView.as_view()(request, organization_code=client_member.organization.code) assertContains(response, "Question has been answered.", status_code=200) - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 def test_answer_question_bad_schema( @@ -217,7 +217,7 @@ def test_ooi_detail_start_scan( ) response = OOIDetailView.as_view()(request, organization_code=client_member.organization.code) - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 assert response.status_code == 200 @@ -263,7 +263,7 @@ def test_ooi_detail_start_scan_no_indemnification( ) response = OOIDetailView.as_view()(request, organization_code=client_member.organization.code) - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 assertContains(response, "Object details") assertContains(response, "Indemnification not present") @@ -295,7 +295,7 @@ def test_ooi_detail_start_scan_no_action( ) response = OOIDetailView.as_view()(request, organization_code=client_member.organization.code) - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 assertContains(response, "Object details") diff --git a/rocky/tests/objects/test_objects_scan_profile.py b/rocky/tests/objects/test_objects_scan_profile.py index 1c2f6fa5aaa..98626ec7407 100644 --- a/rocky/tests/objects/test_objects_scan_profile.py +++ b/rocky/tests/objects/test_objects_scan_profile.py @@ -59,7 +59,7 @@ def test_scan_profile(rf, redteam_member, mock_scheduler, mock_organization_view response = ScanProfileDetailView.as_view()(request, organization_code=redteam_member.organization.code) assert response.status_code == 200 - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 assertContains(response, "Set clearance level") @@ -125,7 +125,7 @@ def test_scan_profile_no_permissions_acknowledged( response = ScanProfileDetailView.as_view()(request, organization_code=redteam_member.organization.code) assert response.status_code == 200 - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 assertNotContains(response, "Set clearance level") @@ -146,7 +146,7 @@ def test_scan_profile_no_permissions_trusted( response = ScanProfileDetailView.as_view()(request, organization_code=redteam_member.organization.code) assert response.status_code == 200 - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 assertNotContains(response, "Set clearance level") @@ -162,7 +162,7 @@ def test_scan_profile_reset_view(rf, redteam_member, mock_scheduler, mock_organi response = ScanProfileResetView.as_view()(request, organization_code=redteam_member.organization.code) assert response.status_code == 200 - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 assertContains(response, "Set clearance level") assertContains(response, "Yes, set to inherit") @@ -183,5 +183,5 @@ def test_scan_reset_calls_octopoes(rf, redteam_member, mock_scheduler, mock_orga response = ScanProfileResetView.as_view()(request, organization_code=redteam_member.organization.code) assert response.status_code == 302 - assert mock_organization_view_octopoes().get_tree.call_count == 2 + assert mock_organization_view_octopoes().get_tree.call_count == 1 assert mock_organization_view_octopoes().save_scan_profile.call_count == 1