Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions ngen/models/announcement.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,32 @@ def send_contact_check_submitted(contact, networks, check):
},
)

@staticmethod
def send_event_sla_reminder(events):
"""
Sends an email reminder for events approaching SLA deadlines.
"""
subject = "[%s] %s" % (
config.TEAM_NAME,
gettext_lazy("SLA Reminder"),
)

template = "reports/event_sla_reminder.html"

Communication.send_mail(
subject,
Communication.render_template(
template,
extra_params={
"events": events,
},
),
{
"to": [config.TEAM_EMAIL],
"from": config.EMAIL_SENDER,
},
)


class Announcement(
AuditModelMixin,
Expand Down
6 changes: 6 additions & 0 deletions ngen/serializers/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,9 @@ class AddressInfoSerializer(serializers.Serializer):
with_entity = serializers.BooleanField(default=True)
with_events = serializers.BooleanField(default=True)
with_cases = serializers.BooleanField(default=True)


class TaskRunSerializer(serializers.Serializer):
task_name = serializers.CharField(max_length=255)
params = serializers.DictField(child=serializers.CharField(), required=False)
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The params field only accepts dictionary values that are strings due to child=serializers.CharField(). This is too restrictive for task parameters which may need integers, booleans, lists, or nested objects. Consider using params = serializers.JSONField(required=False) or params = serializers.DictField(required=False) without the child constraint to allow any JSON-serializable values.

Suggested change
params = serializers.DictField(child=serializers.CharField(), required=False)
params = serializers.JSONField(required=False)

Copilot uses AI. Check for mistakes.
async_run = serializers.BooleanField(default=True)
51 changes: 51 additions & 0 deletions ngen/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,3 +549,54 @@ def send_contact_check_submitted(check_id):
networks=check.contact.networks.all(),
check=check,
)


@shared_task(ignore_result=True, store_errors_even_if_ignored=True)
def event_sla_reminder(minutes=None):
"""
Envia un mail al team con los eventos que estan por superar el SLA.
"""

# Get the interval for the periodic task
if minutes is not None:
try:
minutes = int(minutes)
except ValueError:
raise TaskFailure("Minutes parameter must be an integer.")
timedeltavalue = timezone.timedelta(minutes=minutes)
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable timedeltavalue is not used.

Copilot uses AI. Check for mistakes.
else:
task = PeriodicTask.objects.filter(task="ngen.tasks.event_sla_reminder").first()
if task and task.interval:
interval = task.interval
period = (
interval.period
) # 'seconds', 'minutes', 'hours', 'minutes', 'weeks'
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate 'minutes' in comment should be 'days'. The comment should read: 'seconds', 'minutes', 'hours', 'days', 'weeks'.

Suggested change
) # 'seconds', 'minutes', 'hours', 'minutes', 'weeks'
) # 'seconds', 'minutes', 'hours', 'days', 'weeks'

Copilot uses AI. Check for mistakes.
every = interval.every

# Create a timedelta object based on the interval
timedelta_kwargs = {period: every}
timedeltavalue = timezone.timedelta(**timedelta_kwargs)
else:
default_value = 14
timedeltavalue = timezone.timedelta(minutes=default_value)
logger.warning(
f"No interval found for the periodic task 'ngen.tasks.event_sla_reminder'. Using default value: {default_value} days."
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning message says 'days' but the timedelta is created with minutes=default_value. Change line 581 to timezone.timedelta(days=default_value) or update the warning message to say 'minutes'.

Suggested change
f"No interval found for the periodic task 'ngen.tasks.event_sla_reminder'. Using default value: {default_value} days."
f"No interval found for the periodic task 'ngen.tasks.event_sla_reminder'. Using default value: {default_value} minutes."

Copilot uses AI. Check for mistakes.
)
# search attend SLA
events = (
ngen.models.Event.objects.filter(
case=None
# date__gt=timezone.now() - (F("priority__attend_time") - timedeltavalue),
# date__lte=timezone.now() - F("priority__attend_time"),
)
.exclude(tags__slug="no_sla")
.order_by("priority__severity")
)
Comment on lines +586 to +594
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The critical SLA filtering logic is commented out. This means the query returns ALL unattended events (case=None) regardless of their SLA status. The timedeltavalue parameter is calculated but never used. Either uncomment and fix lines 589-590, or remove the unused parameter calculation if this is intentional.

Copilot uses AI. Check for mistakes.
if events.exists():
Communication.send_event_sla_reminder(events)
return {
"status": "success",
"message": f"SLA reminder sent for {events.count()} events",
}
else:
return {"status": "success", "message": "No events approaching SLA deadlines"}
44 changes: 44 additions & 0 deletions ngen/templates/reports/event_sla_reminder.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{% extends 'reports/content_base.html' %}
{% load i18n %}
{% language lang %}

{% block content_header %}
<div class="content">
<h4>{% translate 'Hello' %},</h4>
<p>
{% blocktranslate trimmed %}
Below are all your unattended events.
{% endblocktranslate %}
</p>
</div>
{% endblock %}

{% block content_body %}
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px;">
<h5>{% translate 'Unnatended Events' %}: {{ events|length }}</h5>
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'Unnatended' to 'Unattended'.

Suggested change
<h5>{% translate 'Unnatended Events' %}: {{ events|length }}</h5>
<h5>{% translate 'Unattended Events' %}: {{ events|length }}</h5>

Copilot uses AI. Check for mistakes.

<table border="1" cellpadding="10" cellspacing="0" class="tsummary tunsolved">
<thead>
<tr>
<th>{% translate 'Date' %}</th>
<th>{% translate 'Event' %}</th>
<th>{% translate 'Priority' %}</th>
<th>{% translate 'Taxonomy' %}</th>
<th>{% translate 'Affected resources' %}</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td class="tcenter" title="{{ event.date | date:'Y-m-d H:i:s' }} UTC">{{ event.date | date:"Y-m-d" }}</td>
<td class="tcenter" title="{{ event.uuid }}">{{ event.uuid|stringformat:"s"|slice:":8" }}</td>
<td class="tcenter" title="{{ event.priority.name }}">{{ event.priority.name|stringformat:"s"|slice:":8" }}</td>
<td class="tcenter" title="{{ event.taxonomy }} ({{ event.taxonomy.type }})">{{ event.taxonomy|stringformat:"s"|slice:":12" }}</td>
<td class="tcenter" title="{{ event.address }}">{{ event.address|stringformat:"s"|slice:":18" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% endlanguage %}
63 changes: 63 additions & 0 deletions ngen/views/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,66 @@ def get(self, request):
},
status=status.HTTP_200_OK,
)


class TaskRunView(APIView):
"""
View to trigger a specific task run manually.
"""

permission_classes = [permissions.IsAdminUser]
serializer_class = serializers.TaskRunSerializer

def post(self, request):
task_name = request.data.get("task_name")
async_run = request.data.get("async_run", True)
params = request.data.get("params", {})
Comment on lines +382 to +384
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The view uses serializer_class but never validates the request data with it. Add serializer validation before processing: serializer = self.serializer_class(data=request.data), serializer.is_valid(raise_exception=True), then use serializer.validated_data instead of accessing request.data directly.

Suggested change
task_name = request.data.get("task_name")
async_run = request.data.get("async_run", True)
params = request.data.get("params", {})
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
task_name = serializer.validated_data.get("task_name")
async_run = serializer.validated_data.get("async_run", True)
params = serializer.validated_data.get("params", {})

Copilot uses AI. Check for mistakes.
available_tasks = [
"attend_cases",
"solve_cases",
"case_renotification",
"contact_summary",
"create_cases_for_matching_events",
"enrich_artifact",
"whois_lookup",
"internal_address_info",
"export_events_for_email_task",
"retest_event_kintun",
"async_send_email",
"retrieve_emails",
"send_contact_checks",
"send_contact_check_reminder",
"send_contact_check_submitted",
"event_sla_reminder",
]
# get function form the module
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'form' to 'from'.

Suggested change
# get function form the module
# get function from the module

Copilot uses AI. Check for mistakes.
task = None
if task_name in available_tasks:
print("Looking for task:", task_name)
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug print statement should be removed or replaced with proper logging using logger.debug() or logger.info().

Copilot uses AI. Check for mistakes.
task = getattr(models.tasks, task_name, None)
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tasks module is not accessible as an attribute of models. This will raise an AttributeError. Import tasks directly at the top of the file with from ngen import tasks and change line 407 to task = getattr(tasks, task_name, None).

Copilot uses AI. Check for mistakes.

if not task:
return Response(
{"message": f"Task {task_name} not found."},
status=status.HTTP_404_NOT_FOUND,
)

# run the task
if async_run:
t = task.delay(**params)
return Response(
{
"task_id": t.id,
"message": f"Task {task_name} triggered successfully.",
},
status=status.HTTP_200_OK,
)
else:
res = task(**params)
return Response(
{
"message": f"Task {task_name} executed successfully.",
"result": res,
},
status=status.HTTP_200_OK,
)
1 change: 1 addition & 0 deletions project/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@
path(
"api/result/<str:task_id>/", views.TaskStatusView.as_view(), name="task_status"
),
path("api/task/run", views.TaskRunView.as_view(), name="task_run"),
path(
"api/version/",
views.VersionView.as_view(),
Expand Down
Loading