Skip to content

Commit

Permalink
Edit report recipe (#3690)
Browse files Browse the repository at this point in the history
Co-authored-by: Rieven <[email protected]>
Co-authored-by: JP Bruins Slot <[email protected]>
Co-authored-by: ammar92 <[email protected]>
Co-authored-by: Jan Klopper <[email protected]>
Co-authored-by: stephanie0x00 <[email protected]>
  • Loading branch information
6 people authored Oct 28, 2024
1 parent 75de529 commit 3c4b5c0
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 46 deletions.
2 changes: 1 addition & 1 deletion octopoes/octopoes/models/ooi/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class ReportRecipe(OOI):
recipe_id: UUID

report_name_format: str
subreport_name_format: str
subreport_name_format: str | None = None

input_recipe: dict[str, Any] # can contain a query which maintains a live set of OOIs or manually picked OOIs.
report_types: list[str]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,18 @@
</td>
<td class="nowrap">{{ schedule.deadline_at }}</td>
<td class="nowrap">{{ schedule.cron }}</td>
{% if schedule.reports %}
<td class="actions">
<button class="expando-button"
data-icon-open-class="icon ti-chevron-down"
data-icon-close-class="icon ti-chevron-up"
data-close-label="{% translate "Close details" %}">
{% translate "Open details" %}
</button>
</td>
{% else %}
<td></td>
{% endif %}
<td class="actions">
<button class="expando-button"
data-icon-open-class="icon ti-chevron-down"
data-icon-close-class="icon ti-chevron-up"
data-close-label="{% translate "Close details" %}">
{% translate "Open details" %}
</button>
</td>
</tr>
{% if schedule.reports %}
<tr class="expando-row">
<td colspan="6">
<tr class="expando-row">
<td colspan="6">
{% if schedule.reports %}
<table>
<caption class="visually-hidden">{% translate "Scheduled Reports:" %}</caption>
<thead>
Expand Down Expand Up @@ -83,9 +79,13 @@
{% endfor %}
</tbody>
</table>
</td>
</tr>
{% endif %}
{% endif %}
<div class="horizontal-view toolbar">
<a class="button ghost"
href="{% ooi_url "ooi_edit" schedule.recipe organization.code %}"><span aria-hidden="true" class="icon ti-edit action-button"></span>{% translate "Edit report recipe" %}</a>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
Expand Down
34 changes: 18 additions & 16 deletions rocky/reports/views/report_overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,24 @@ def get_queryset(self) -> list[dict[str, Any]]:
recipes = []
if report_schedules:
for schedule in report_schedules:
recipe_id = schedule["data"]["report_recipe_id"]
# TODO: This is a workaround to get the recipes and reports.
# We should create an endpoint for this in octopoes
recipe_ooi_tree = self.get_recipe_ooi_tree(recipe_id)
if recipe_ooi_tree is not None:
recipe_tree = recipe_ooi_tree.store.values()
recipe_ooi = [ooi for ooi in recipe_tree if isinstance(ooi, ReportRecipe)][0]
report_oois = [ooi for ooi in recipe_tree if isinstance(ooi, Report)]
recipes.append(
{
"recipe": recipe_ooi,
"cron": schedule["schedule"],
"deadline_at": datetime.fromisoformat(schedule["deadline_at"]),
"reports": report_oois,
}
)
if schedule["data"]:
recipe_id = schedule["data"]["report_recipe_id"]
# TODO: This is a workaround to get the recipes and reports.
# We should create an endpoint for this in octopoes
recipe_ooi_tree = self.get_recipe_ooi_tree(recipe_id)
if recipe_ooi_tree is not None:
recipe_tree = recipe_ooi_tree.store.values()
recipe_ooi = next(ooi for ooi in recipe_tree if isinstance(ooi, ReportRecipe))
report_oois = [ooi for ooi in recipe_tree if isinstance(ooi, Report)]
recipes.append(
{
"schedule_id": schedule["id"],
"recipe": recipe_ooi,
"cron": schedule["schedule"],
"deadline_at": datetime.fromisoformat(schedule["deadline_at"]),
"reports": report_oois,
}
)

return recipes

Expand Down
6 changes: 5 additions & 1 deletion rocky/rocky/locale/django.pot
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-21 16:21+0000\n"
"POT-Creation-Date: 2024-10-28 12:21+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -4186,6 +4186,10 @@ msgstr ""
msgid "Show report details"
msgstr ""

#: reports/templates/report_overview/scheduled_reports_table.html
msgid "Edit report recipe"
msgstr ""

#: reports/templates/report_overview/scheduled_reports_table.html
msgid "No scheduled reports have been generated yet."
msgstr ""
Expand Down
15 changes: 15 additions & 0 deletions rocky/rocky/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,21 @@ def get_schedule_details(self, schedule_id: str) -> ScheduleResponse:
except ConnectError:
raise SchedulerConnectError()

def post_schedule_search(self, filters: dict[str, str]) -> PaginatedSchedulesResponse:
try:
res = self._client.post("/schedules/search", json=filters)
res.raise_for_status()
return PaginatedSchedulesResponse.model_validate_json(res.content)
except ConnectError:
raise SchedulerConnectError()

def patch_schedule(self, schedule_id: str, params: dict[str, Any]) -> None:
try:
response = self._client.patch(f"/schedules/{schedule_id}", json=params)
response.raise_for_status()
except (HTTPStatusError, ConnectError):
raise SchedulerHTTPError()

def post_schedule(self, schedule: ScheduleRequest) -> ScheduleResponse:
try:
res = self._client.post("/schedules", json=schedule.model_dump(exclude_none=True))
Expand Down
24 changes: 23 additions & 1 deletion rocky/rocky/views/ooi_edit.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from datetime import datetime, timezone
from enum import Enum

from django.utils.translation import gettext_lazy as _
from tools.view_helpers import get_ooi_url

from rocky.views.ooi_view import BaseOOIFormView
from rocky.views.scheduler import SchedulerView


class OOIEditView(BaseOOIFormView):
class OOIEditView(BaseOOIFormView, SchedulerView):
template_name = "oois/ooi_edit.html"
task_type = "report"

def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
Expand Down Expand Up @@ -36,6 +39,25 @@ def get_form_kwargs(self):

return kwargs

def form_valid(self, form):
form_data = form.cleaned_data
report_recipe_id = form_data.get("recipe_id")
cron_expression = form_data.get("cron_expression")

# If the cron_expression of the ReportRecipe is changed, the scheduler must also be updated
if report_recipe_id and cron_expression:
deadline_at = datetime.now(timezone.utc).isoformat()
filters = {
"filters": [
{"column": "data", "field": "report_recipe_id", "operator": "eq", "value": report_recipe_id}
]
}

schedule_id = str(self.get_schedule_with_filters(filters).id)
self.edit_report_schedule(schedule_id, {"schedule": cron_expression, "deadline_at": deadline_at})

return super().form_valid(form)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

Expand Down
9 changes: 9 additions & 0 deletions rocky/rocky/views/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ def create_report_schedule(self, report_recipe: ReportRecipe, deadline_at: str)
except SchedulerError as error:
return messages.error(self.request, error.message)

def edit_report_schedule(self, schedule_id: str, params):
self.scheduler_client.patch_schedule(schedule_id=schedule_id, params=params)

def get_report_schedules(self) -> list[dict[str, Any]]:
try:
return self.scheduler_client.get_scheduled_reports(scheduler_id=self.scheduler_id)
Expand Down Expand Up @@ -180,6 +183,12 @@ def get_json_task_details(self) -> JsonResponse | None:
except SchedulerError as error:
return messages.error(self.request, error.message)

def get_schedule_with_filters(self, filters: dict[str, Any]) -> ScheduleResponse:
try:
return self.scheduler_client.post_schedule_search(filters).results[0]
except SchedulerError as error:
return messages.error(self.request, error.message)

def schedule_task(self, task: Task) -> None:
if not self.indemnification_present:
return self.indemnification_error()
Expand Down
101 changes: 100 additions & 1 deletion rocky/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from octopoes.models.ooi.dns.zone import Hostname
from octopoes.models.ooi.findings import CVEFindingType, Finding, KATFindingType, RiskLevelSeverity
from octopoes.models.ooi.network import IPAddressV4, IPAddressV6, IPPort, Network, Protocol
from octopoes.models.ooi.reports import Report
from octopoes.models.ooi.reports import Report, ReportRecipe
from octopoes.models.ooi.service import IPService, Service
from octopoes.models.ooi.software import Software
from octopoes.models.ooi.web import URL, SecurityTXT, Website
Expand Down Expand Up @@ -1310,6 +1310,105 @@ def get_subreports() -> list[tuple[str, Report]]:
]


@pytest.fixture
def report_recipe():
return ReportRecipe(
recipe_id="744d054e-9c70-4f18-ad27-122cfc1b7903",
report_name_format="Test Report Name Format",
subreport_name_format="Test Subreport Name Format",
input_recipe={"input_oois": ["Hostname|internet|mispo.es"]},
report_types=["dns-report"],
cron_expression="0 0 * * *",
)


@pytest.fixture
def get_report_schedules():
report_schedule = [
{
"id": "06a783b3-af62-429d-8b48-5934c8702366",
"scheduler_id": "report-madelon123",
"hash": "f429f60fda539f9017544758163a955e",
"data": {
"type": "report",
"organisation_id": "madelon123",
"report_recipe_id": "6a121a53-f795-4bfe-9aea-f410a96fca59",
},
"enabled": "true",
"schedule": "0 * * * *",
"tasks": [
{
"id": "21ce00b8-57c4-4d65-a53e-275b4e44da6f",
"scheduler_id": "report-madelon123",
"schedule_id": "06a783b3-af62-429d-8b48-5934c8702366",
"priority": 1729520184,
"status": "completed",
"type": "report",
"hash": "f429f60fda539f9017544758163a955e",
"data": {
"organisation_id": "madelon123",
"report_recipe_id": "6a121a53-f795-4bfe-9aea-f410a96fca59",
},
"created_at": "2024-10-21T14:16:24.702759Z",
"modified_at": "2024-10-21T14:16:24.702761Z",
},
{
"id": "f63c2224-a4b2-4161-9790-9b4d665a4683",
"scheduler_id": "report-madelon123",
"schedule_id": "06a783b3-af62-429d-8b48-5934c8702366",
"priority": 1729580020,
"status": "completed",
"type": "report",
"hash": "f429f60fda539f9017544758163a955e",
"data": {
"organisation_id": "madelon123",
"report_recipe_id": "6a121a53-f795-4bfe-9aea-f410a96fca59",
},
"created_at": "2024-10-22T06:53:40.405185Z",
"modified_at": "2024-10-22T06:53:40.405192Z",
},
],
"deadline_at": "2024-10-22T12:00:00Z",
"created_at": "2024-10-21T14:14:00.359039Z",
"modified_at": "2024-10-21T14:14:00.359043Z",
},
{
"id": "e9a00bc1-850a-401a-89d9-252d98823bb3",
"scheduler_id": "report-madelon123",
"hash": "02e67c8676cb8135681d52ca62a9fe5b",
"data": {
"type": "report",
"organisation_id": "madelon123",
"report_recipe_id": "31c79490-fb51-440d-9108-0c388276f655",
},
"enabled": "true",
"schedule": "0 0 * * *",
"tasks": [
{
"id": "f4f938e0-5f2d-4bcd-9b28-11831b7835e4",
"scheduler_id": "report-madelon123",
"schedule_id": "e9a00bc1-850a-401a-89d9-252d98823bb3",
"priority": 1729521145,
"status": "completed",
"type": "report",
"hash": "02e67c8676cb8135681d52ca62a9fe5b",
"data": {
"organisation_id": "madelon123",
"report_recipe_id": "31c79490-fb51-440d-9108-0c388276f655",
},
"created_at": "2024-10-21T14:32:25.247525Z",
"modified_at": "2024-10-21T14:32:25.247527Z",
}
],
"deadline_at": "2024-10-23T00:00:00Z",
"created_at": "2024-10-21T13:34:42.791561Z",
"modified_at": "2024-10-21T13:34:42.791563Z",
},
]

return report_schedule


def setup_request(request, user):
request = SessionMiddleware(lambda r: r)(request)
request.session[DEVICE_ID_SESSION_KEY] = user.staticdevice_set.get().persistent_id
Expand Down
49 changes: 49 additions & 0 deletions rocky/tests/objects/test_objects_edit.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import json

from django.urls import reverse
from pytest_django.asserts import assertContains

from rocky.views.ooi_edit import OOIEditView
Expand All @@ -13,3 +16,49 @@ def test_ooi_edit(rf, client_member, mock_organization_view_octopoes, network):
assert response.status_code == 200
assertContains(response, "testnetwork")
assertContains(response, "Save Network")


def test_ooi_edit_report_recipe_get(rf, client_member, mock_organization_view_octopoes, report_recipe):
mock_organization_view_octopoes().get.return_value = report_recipe
ooi_id = f"ReportRecipe|{report_recipe.recipe_id}"

request = setup_request(rf.get("ooi_edit", {"ooi_id": ooi_id}), client_member.user)
response = OOIEditView.as_view()(request, organization_code=client_member.organization.code)

assert response.status_code == 200
assertContains(response, "Edit ReportRecipe: " + ooi_id)


def test_ooi_edit_report_recipe_post(
rf, client_member, mock_organization_view_octopoes, report_recipe, mocker, mock_scheduler
):
mock_organization_view_octopoes().get.return_value = report_recipe
mocker.patch("rocky.views.ooi_view.create_ooi")
ooi_id = f"ReportRecipe|{report_recipe.recipe_id}"

request_url = (
reverse("ooi_edit", kwargs={"organization_code": client_member.organization.code}) + f"?ooi_id={ooi_id}"
)

request = setup_request(
rf.post(
request_url,
{
"ooi_type": "ReportRecipe",
"user": client_member.user.email,
"recipe_id": report_recipe.recipe_id,
"report_name_format": report_recipe.report_name_format,
"subreport_name_format": report_recipe.subreport_name_format,
"input_recipe": json.dumps(report_recipe.input_recipe),
"report_types": json.dumps(report_recipe.report_types),
"cron_expression": report_recipe.cron_expression,
},
),
client_member.user,
)
response = OOIEditView.as_view()(request, organization_code=client_member.organization.code)

assert response.status_code == 302

response_url = "/en/{}/objects/detail/?ooi_id=ReportRecipe%7C{}"
assert response.url == response_url.format(client_member.organization.code, report_recipe.recipe_id)
Loading

0 comments on commit 3c4b5c0

Please sign in to comment.