From 743a63b64e412562ac5a8603c0d7de8d349ef4fe Mon Sep 17 00:00:00 2001 From: Josema Camacho Date: Wed, 2 Sep 2026 11:39:02 +0200 Subject: [PATCH] fix(api): address Jira dedup review feedback --- .../api/v1/serializer_utils/integrations.py | 19 +++++++++++++++++++ api/src/backend/api/v1/serializers.py | 16 +++------------- .../backend/tasks/tests/test_integrations.py | 1 + .../prowler-app-jira-integration.mdx | 2 +- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/api/src/backend/api/v1/serializer_utils/integrations.py b/api/src/backend/api/v1/serializer_utils/integrations.py index aa941b6c2a..66821ebf05 100644 --- a/api/src/backend/api/v1/serializer_utils/integrations.py +++ b/api/src/backend/api/v1/serializer_utils/integrations.py @@ -1,5 +1,6 @@ import os import re +from typing import Any from api.models import Integration, IntegrationProviderRelationship, Provider from api.v1.serializer_utils.base import BaseValidateSerializer @@ -12,6 +13,24 @@ ATLASSIAN_SITE_NAME_REGEX = re.compile( ) +def sanitize_integration_task_result(task_name: str | None, result: Any) -> Any: + """Remove private integration metadata from task results.""" + if task_name != "integration-jira" or not isinstance(result, dict): + return result + + skipped = result.get("skipped") + if not isinstance(skipped, list): + return result + + sanitized_result = result.copy() + sanitized_result["skipped"] = [ + {"finding_id": entry["finding_id"]} + for entry in skipped + if isinstance(entry, dict) and "finding_id" in entry + ] + return sanitized_result + + def replace_integration_providers( integration: Integration, providers: list[Provider], tenant_id: str ) -> None: diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index b558a36f24..082e89a3ee 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -51,6 +51,7 @@ from api.v1.serializer_utils.integrations import ( S3ConfigSerializer, SecurityHubConfigSerializer, replace_integration_providers, + sanitize_integration_task_result, ) from api.v1.serializer_utils.lighthouse import ( BedrockCredentialsSerializer, @@ -639,19 +640,8 @@ class TaskSerializer(RLSSerializer, TaskBase): @extend_schema_field(serializers.JSONField()) def get_result(self, obj): result = self.get_json_field(obj, "result") - if ( - isinstance(result, dict) - and obj.task_runner_task - and obj.task_runner_task.task_name == "integration-jira" - ): - skipped = result.get("skipped") - if isinstance(skipped, list): - result["skipped"] = [ - {"finding_id": entry["finding_id"]} - for entry in skipped - if isinstance(entry, dict) and "finding_id" in entry - ] - return result + task_name = obj.task_runner_task.task_name if obj.task_runner_task else None + return sanitize_integration_task_result(task_name, result) @extend_schema_field(serializers.JSONField()) def get_task_args(self, obj): diff --git a/api/src/backend/tasks/tests/test_integrations.py b/api/src/backend/tasks/tests/test_integrations.py index 0fd6c82949..2c39203428 100644 --- a/api/src/backend/tasks/tests/test_integrations.py +++ b/api/src/backend/tasks/tests/test_integrations.py @@ -1690,6 +1690,7 @@ class TestJiraIntegration: _refresh_jira_issue_statuses=MagicMock(return_value={}), _reserve_initial_jira_issue=MagicMock(return_value=reservation), _reserve_jira_issue_replacement=MagicMock(return_value=reservation), + _start_jira_delivery_attempt=MagicMock(return_value=True), _link_jira_issue=MagicMock(return_value=True), _release_jira_delivery_attempt=MagicMock(return_value=True), ): diff --git a/docs/user-guide/tutorials/prowler-app-jira-integration.mdx b/docs/user-guide/tutorials/prowler-app-jira-integration.mdx index f071e90301..6668f688ff 100644 --- a/docs/user-guide/tutorials/prowler-app-jira-integration.mdx +++ b/docs/user-guide/tutorials/prowler-app-jira-integration.mdx @@ -147,7 +147,7 @@ Prowler remembers each confirmed Jira issue created for a Finding, keyed by the * If the linked issue has a Jira status in the **Done** category, Prowler creates a replacement and links the Finding to the new issue after Jira confirms it. * If Jira reports that the issue is missing or forbidden, or its status cannot be determined safely, Prowler keeps the existing link and skips the Finding. -The link, and the last status observed in Jira, are available through the API at `GET /api/v1/jira-issues` (filter by `finding_uid`, `finding_uid__in`, `provider_id`, `integration` or `issue_key`). Each Jira integration keeps its own links, so the same Finding can have one issue per integration. +The link, and the last status observed in Jira, are available through the API at `GET /api/v1/jira-issues` (filter by `finding_uid`, `finding_uid__in`, `provider_id`, `integration`, `issue_key`, `issue_status_category` or `issue_status_category__in`). Each Jira integration keeps its own links, so the same Finding can have one issue per integration. Prowler uses a private delivery marker to check whether Jira created an issue when a delivery result is uncertain. A reservation that stopped before the delivery attempt began can be resumed safely by a later explicit send. After an attempt starts, Prowler searches Jira by the marker: exactly one matching issue completes the link, while no match, multiple matches, or a lookup error keeps the reservation and prevents an automatic retry. A process failure after the attempt starts but before Jira receives the request can still require manual investigation because Prowler cannot prove whether Jira accepted the request.