mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
refactor(integrations-jira): Move domain to credentials and retrieve metadata during connection test (#8637)
This commit is contained in:
committed by
GitHub
parent
b15e3d339c
commit
0463fd0830
+1
-1
@@ -5,7 +5,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
## [1.13.0] (Prowler UNRELEASED)
|
||||
|
||||
### Added
|
||||
- Integration with JIRA, enabling sending findings to a JIRA project [(#8622)](https://github.com/prowler-cloud/prowler/pull/8622)
|
||||
- Integration with JIRA, enabling sending findings to a JIRA project [(#8622)](https://github.com/prowler-cloud/prowler/pull/8622), [(#8637)](https://github.com/prowler-cloud/prowler/pull/8637)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9134,23 +9134,10 @@ components:
|
||||
the current execution.
|
||||
- type: object
|
||||
title: JIRA
|
||||
properties:
|
||||
project_key:
|
||||
type: string
|
||||
description: The JIRA project key where issues will be created
|
||||
(e.g., 'PROJ', 'SEC').
|
||||
issue_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of JIRA issue types to create for findings.
|
||||
issue_labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of labels to apply to created JIRA issues..
|
||||
required:
|
||||
- project_key
|
||||
description: JIRA integration does not accept any configuration in
|
||||
the payload. Leave it as an empty JSON object (`{}`).
|
||||
properties: {}
|
||||
additionalProperties: false
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -9197,17 +9184,17 @@ components:
|
||||
type: string
|
||||
format: email
|
||||
description: The email address of the JIRA user account.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
api_token:
|
||||
type: string
|
||||
description: The API token for authentication with JIRA. This
|
||||
can be generated from your Atlassian account settings.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
- domain
|
||||
writeOnly: true
|
||||
required:
|
||||
- integration_type
|
||||
@@ -9331,23 +9318,10 @@ components:
|
||||
in the current execution.
|
||||
- type: object
|
||||
title: JIRA
|
||||
properties:
|
||||
project_key:
|
||||
type: string
|
||||
description: The JIRA project key where issues will be created
|
||||
(e.g., 'PROJ', 'SEC').
|
||||
issue_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of JIRA issue types to create for findings.
|
||||
issue_labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of labels to apply to created JIRA issues..
|
||||
required:
|
||||
- project_key
|
||||
description: JIRA integration does not accept any configuration
|
||||
in the payload. Leave it as an empty JSON object (`{}`).
|
||||
properties: {}
|
||||
additionalProperties: false
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -9395,17 +9369,17 @@ components:
|
||||
type: string
|
||||
format: email
|
||||
description: The email address of the JIRA user account.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
api_token:
|
||||
type: string
|
||||
description: The API token for authentication with JIRA. This
|
||||
can be generated from your Atlassian account settings.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
- domain
|
||||
writeOnly: true
|
||||
required:
|
||||
- integration_type
|
||||
@@ -9544,23 +9518,10 @@ components:
|
||||
the current execution.
|
||||
- type: object
|
||||
title: JIRA
|
||||
properties:
|
||||
project_key:
|
||||
type: string
|
||||
description: The JIRA project key where issues will be created
|
||||
(e.g., 'PROJ', 'SEC').
|
||||
issue_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of JIRA issue types to create for findings.
|
||||
issue_labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of labels to apply to created JIRA issues..
|
||||
required:
|
||||
- project_key
|
||||
description: JIRA integration does not accept any configuration in
|
||||
the payload. Leave it as an empty JSON object (`{}`).
|
||||
properties: {}
|
||||
additionalProperties: false
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -9607,17 +9568,17 @@ components:
|
||||
type: string
|
||||
format: email
|
||||
description: The email address of the JIRA user account.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
api_token:
|
||||
type: string
|
||||
description: The API token for authentication with JIRA. This
|
||||
can be generated from your Atlassian account settings.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
- domain
|
||||
writeOnly: true
|
||||
relationships:
|
||||
type: object
|
||||
@@ -10986,23 +10947,10 @@ components:
|
||||
in the current execution.
|
||||
- type: object
|
||||
title: JIRA
|
||||
properties:
|
||||
project_key:
|
||||
type: string
|
||||
description: The JIRA project key where issues will be created
|
||||
(e.g., 'PROJ', 'SEC').
|
||||
issue_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of JIRA issue types to create for findings.
|
||||
issue_labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of labels to apply to created JIRA issues..
|
||||
required:
|
||||
- project_key
|
||||
description: JIRA integration does not accept any configuration
|
||||
in the payload. Leave it as an empty JSON object (`{}`).
|
||||
properties: {}
|
||||
additionalProperties: false
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -11050,17 +10998,17 @@ components:
|
||||
type: string
|
||||
format: email
|
||||
description: The email address of the JIRA user account.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
api_token:
|
||||
type: string
|
||||
description: The API token for authentication with JIRA. This
|
||||
can be generated from your Atlassian account settings.
|
||||
domain:
|
||||
type: string
|
||||
description: The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
- domain
|
||||
writeOnly: true
|
||||
relationships:
|
||||
type: object
|
||||
|
||||
@@ -503,28 +503,38 @@ class TestProwlerIntegrationConnectionTest:
|
||||
assert integration.configuration["regions"]["ap-south-1"] is False
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@patch("api.utils.rls_transaction")
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_success_basic_auth(self, mock_jira_class):
|
||||
def test_jira_connection_success_basic_auth(
|
||||
self, mock_jira_class, mock_rls_transaction
|
||||
):
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.tenant_id = "test-tenant-id"
|
||||
integration.credentials = {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test_api_token",
|
||||
}
|
||||
integration.configuration = {
|
||||
"domain": "example.atlassian.net",
|
||||
}
|
||||
integration.configuration = {}
|
||||
|
||||
# Mock successful JIRA connection with projects
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = True
|
||||
mock_connection.error = None
|
||||
mock_connection.projects = {"PROJ1": "Project 1", "PROJ2": "Project 2"}
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
# Mock rls_transaction context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is True
|
||||
assert result.error is None
|
||||
|
||||
# Verify JIRA connection was called with correct parameters including domain from credentials
|
||||
mock_jira_class.test_connection.assert_called_once_with(
|
||||
user_mail="test@example.com",
|
||||
api_token="test_api_token",
|
||||
@@ -532,32 +542,112 @@ class TestProwlerIntegrationConnectionTest:
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
# Verify rls_transaction was called with correct tenant_id
|
||||
mock_rls_transaction.assert_called_once_with("test-tenant-id")
|
||||
|
||||
# Verify projects were saved to integration configuration
|
||||
assert integration.configuration["projects"] == {
|
||||
"PROJ1": "Project 1",
|
||||
"PROJ2": "Project 2",
|
||||
}
|
||||
|
||||
# Verify integration.save() was called
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@patch("api.utils.rls_transaction")
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_failure_invalid_credentials(self, mock_jira_class):
|
||||
def test_jira_connection_failure_invalid_credentials(
|
||||
self, mock_jira_class, mock_rls_transaction
|
||||
):
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.tenant_id = "test-tenant-id"
|
||||
integration.credentials = {
|
||||
"user_mail": "invalid@example.com",
|
||||
"api_token": "invalid_token",
|
||||
}
|
||||
integration.configuration = {
|
||||
"domain": "invalid.atlassian.net",
|
||||
}
|
||||
integration.configuration = {}
|
||||
|
||||
# Mock failed JIRA connection
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = False
|
||||
mock_connection.error = Exception("Authentication failed: Invalid credentials")
|
||||
mock_connection.projects = {} # Empty projects when connection fails
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
# Mock rls_transaction context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is False
|
||||
assert "Authentication failed: Invalid credentials" in str(result.error)
|
||||
|
||||
# Verify JIRA connection was called with correct parameters
|
||||
mock_jira_class.test_connection.assert_called_once_with(
|
||||
user_mail="invalid@example.com",
|
||||
api_token="invalid_token",
|
||||
domain="invalid.atlassian.net",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
# Verify rls_transaction was called even on failure
|
||||
mock_rls_transaction.assert_called_once_with("test-tenant-id")
|
||||
|
||||
# Verify empty projects dict was saved to integration configuration
|
||||
assert integration.configuration["projects"] == {}
|
||||
|
||||
# Verify integration.save() was called even on connection failure
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@patch("api.utils.rls_transaction")
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_projects_update_with_existing_configuration(
|
||||
self, mock_jira_class, mock_rls_transaction
|
||||
):
|
||||
"""Test that projects are properly updated when integration already has configuration data"""
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.tenant_id = "test-tenant-id"
|
||||
integration.credentials = {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test_api_token",
|
||||
"domain": "example.atlassian.net",
|
||||
}
|
||||
integration.configuration = {
|
||||
"issue_types": ["Bug", "Task"], # Existing configuration
|
||||
"projects": {"OLD_PROJ": "Old Project"}, # Will be overwritten
|
||||
}
|
||||
|
||||
# Mock successful JIRA connection with new projects
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = True
|
||||
mock_connection.error = None
|
||||
mock_connection.projects = {
|
||||
"NEW_PROJ1": "New Project 1",
|
||||
"NEW_PROJ2": "New Project 2",
|
||||
}
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
# Mock rls_transaction context manager
|
||||
mock_rls_transaction.return_value.__enter__ = MagicMock()
|
||||
mock_rls_transaction.return_value.__exit__ = MagicMock()
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is True
|
||||
assert result.error is None
|
||||
|
||||
# Verify projects were updated (old projects replaced with new ones)
|
||||
assert integration.configuration["projects"] == {
|
||||
"NEW_PROJ1": "New Project 1",
|
||||
"NEW_PROJ2": "New Project 2",
|
||||
}
|
||||
|
||||
# Verify other configuration fields were preserved
|
||||
assert integration.configuration["issue_types"] == ["Bug", "Task"]
|
||||
|
||||
# Verify integration.save() was called
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@@ -5660,18 +5660,6 @@ class TestIntegrationViewSet:
|
||||
},
|
||||
{},
|
||||
),
|
||||
# JIRA
|
||||
(
|
||||
Integration.IntegrationChoices.JIRA,
|
||||
{
|
||||
"project_key": "JIRA",
|
||||
"domain": "prowlerdomain",
|
||||
},
|
||||
{
|
||||
"api_token": "this-is-an-api-token-for-jira-that-works-for-sure",
|
||||
"user_mail": "testing@prowler.com",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_integrations_create_valid(
|
||||
@@ -5720,6 +5708,47 @@ class TestIntegrationViewSet:
|
||||
== data["data"]["relationships"]["providers"]["data"][0]["id"]
|
||||
)
|
||||
|
||||
def test_integrations_create_valid_jira(
|
||||
self,
|
||||
authenticated_client,
|
||||
):
|
||||
"""Jira integrations are special"""
|
||||
data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"domain": "prowlerdomain",
|
||||
"api_token": "this-is-an-api-token-for-jira-that-works-for-sure",
|
||||
"user_mail": "testing@prowler.com",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert Integration.objects.count() == 1
|
||||
integration = Integration.objects.first()
|
||||
integration_configuration = response.json()["data"]["attributes"][
|
||||
"configuration"
|
||||
]
|
||||
assert "projects" in integration_configuration
|
||||
assert "issue_types" in integration_configuration
|
||||
assert "domain" in integration_configuration
|
||||
assert integration.enabled == data["data"]["attributes"]["enabled"]
|
||||
assert (
|
||||
integration.integration_type
|
||||
== data["data"]["attributes"]["integration_type"]
|
||||
)
|
||||
assert "credentials" not in response.json()["data"]["attributes"]
|
||||
|
||||
def test_integrations_create_valid_relationships(
|
||||
self,
|
||||
authenticated_client,
|
||||
@@ -5822,31 +5851,37 @@ class TestIntegrationViewSet:
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {
|
||||
"project_key": "JIRA",
|
||||
"projects": ["JIRA"],
|
||||
},
|
||||
"credentials": {},
|
||||
"credentials": {"domain": "prowlerdomain"},
|
||||
},
|
||||
"required",
|
||||
"domain",
|
||||
"invalid",
|
||||
"configuration",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {
|
||||
"project_key": "JIRA",
|
||||
"credentialss": {
|
||||
"domain": "prowlerdomain",
|
||||
"api_token": "api-token",
|
||||
"user_mail": "test@prowler.com",
|
||||
},
|
||||
},
|
||||
"required",
|
||||
"configuration",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {},
|
||||
},
|
||||
"required",
|
||||
"credentials",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {
|
||||
"project_key": "JIRA",
|
||||
"domain": "prowlerdomain",
|
||||
},
|
||||
"configuration": {},
|
||||
"credentials": {"api_token": "api-token"},
|
||||
},
|
||||
"invalid",
|
||||
@@ -6093,32 +6128,21 @@ class TestIntegrationViewSet:
|
||||
== "/data/attributes/configuration"
|
||||
)
|
||||
|
||||
def test_integrations_create_duplicate_jira(
|
||||
self, authenticated_client, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
|
||||
def test_integrations_create_duplicate_jira(self, authenticated_client):
|
||||
# Create first JIRA integration
|
||||
data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {
|
||||
"project_key": "TEST",
|
||||
"domain": "test.atlassian.net",
|
||||
},
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test-api-token",
|
||||
"domain": "prowlerdomain",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
"relationships": {
|
||||
"providers": {
|
||||
"data": [{"type": "providers", "id": str(provider.id)}]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6145,6 +6169,124 @@ class TestIntegrationViewSet:
|
||||
== "/data/attributes/configuration"
|
||||
)
|
||||
|
||||
def test_integrations_update_jira_configuration_readonly(
|
||||
self, authenticated_client
|
||||
):
|
||||
# Create JIRA integration first
|
||||
create_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test-api-token",
|
||||
"domain": "initial-domain",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Create the integration
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(create_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
integration_id = response.json()["data"]["id"]
|
||||
|
||||
# Attempt to update configuration - should be ignored/not allowed
|
||||
update_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"id": integration_id,
|
||||
"attributes": {
|
||||
"configuration": {
|
||||
"projects": {"NEW_PROJECT": "New Project"},
|
||||
"issue_types": ["Epic", "Story"],
|
||||
"domain": "malicious-domain",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.patch(
|
||||
reverse("integration-detail", kwargs={"pk": integration_id}),
|
||||
data=json.dumps(update_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_integrations_update_jira_credentials_domain_reflects_in_configuration(
|
||||
self, authenticated_client
|
||||
):
|
||||
# Create JIRA integration first
|
||||
create_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {},
|
||||
"credentials": {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test-api-token",
|
||||
"domain": "original-domain",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Create the integration
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(create_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
integration_id = response.json()["data"]["id"]
|
||||
|
||||
# Verify initial domain in configuration
|
||||
initial_integration = response.json()["data"]
|
||||
assert (
|
||||
initial_integration["attributes"]["configuration"]["domain"]
|
||||
== "original-domain"
|
||||
)
|
||||
|
||||
# Update credentials with new domain
|
||||
update_data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"id": integration_id,
|
||||
"attributes": {
|
||||
"credentials": {
|
||||
"user_mail": "updated@example.com",
|
||||
"api_token": "updated-api-token",
|
||||
"domain": "updated-domain",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
response = authenticated_client.patch(
|
||||
reverse("integration-detail", kwargs={"pk": integration_id}),
|
||||
data=json.dumps(update_data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Verify the new domain is reflected in configuration
|
||||
updated_integration = response.json()["data"]
|
||||
configuration = updated_integration["attributes"]["configuration"]
|
||||
assert configuration["domain"] == "updated-domain"
|
||||
|
||||
# Verify other configuration fields are preserved
|
||||
assert "projects" in configuration
|
||||
assert "issue_types" in configuration
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSAMLTokenValidation:
|
||||
|
||||
@@ -6,10 +6,11 @@ from django.db.models import Subquery
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.db_utils import rls_transaction
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Integration, Invitation, Processor, Provider, Resource
|
||||
from api.v1.serializers import FindingMetadataSerializer
|
||||
from prowler.lib.outputs.jira.jira import Jira
|
||||
from prowler.lib.outputs.jira.jira import Jira, JiraBasicAuthError, JiraNoProjectsError
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.aws.lib.s3.s3 import S3
|
||||
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub
|
||||
@@ -238,11 +239,15 @@ def prowler_integration_connection_test(integration: Integration) -> Connection:
|
||||
|
||||
return connection
|
||||
elif integration.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
return Jira.test_connection(
|
||||
jira_connection = Jira.test_connection(
|
||||
**integration.credentials,
|
||||
domain=integration.configuration["domain"],
|
||||
raise_on_exception=False,
|
||||
)
|
||||
project_keys = jira_connection.projects if jira_connection.is_connected else {}
|
||||
with rls_transaction(str(integration.tenant_id)):
|
||||
integration.configuration["projects"] = project_keys
|
||||
integration.save()
|
||||
return jira_connection
|
||||
elif integration.integration_type == Integration.IntegrationChoices.SLACK:
|
||||
pass
|
||||
else:
|
||||
@@ -342,3 +347,26 @@ def get_findings_metadata_no_aggregations(tenant_id: str, filtered_queryset):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
return serializer.data
|
||||
|
||||
|
||||
def initialize_prowler_integration(integration: Integration) -> Jira:
|
||||
# TODO Refactor other integrations to use this function
|
||||
if integration.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
try:
|
||||
return Jira(
|
||||
**integration.credentials, domain=integration.configuration["domain"]
|
||||
)
|
||||
except JiraBasicAuthError as jira_auth_error:
|
||||
with rls_transaction(str(integration.tenant_id)):
|
||||
integration.connected = False
|
||||
integration.connection_last_checked_at = datetime.now(tz=timezone.utc)
|
||||
integration.save()
|
||||
raise jira_auth_error
|
||||
|
||||
|
||||
def get_jira_integration_metadata(jira_integration: Integration) -> dict:
|
||||
prowler_jira = initialize_prowler_integration(jira_integration)
|
||||
try:
|
||||
return prowler_jira.get_jira_metadata()
|
||||
except JiraNoProjectsError:
|
||||
return {}
|
||||
|
||||
@@ -68,10 +68,11 @@ class SecurityHubConfigSerializer(BaseValidateSerializer):
|
||||
|
||||
|
||||
class JiraConfigSerializer(BaseValidateSerializer):
|
||||
project_key = serializers.CharField(required=True)
|
||||
domain = serializers.CharField(required=True)
|
||||
issue_types = serializers.ListField(required=False, child=serializers.CharField())
|
||||
issue_labels = serializers.ListField(required=False, child=serializers.CharField())
|
||||
domain = serializers.CharField(read_only=True)
|
||||
issue_types = serializers.ListField(
|
||||
read_only=True, child=serializers.CharField(), default=["Task", "Bug"]
|
||||
)
|
||||
projects = serializers.DictField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
resource_name = "integrations"
|
||||
@@ -95,6 +96,7 @@ class AWSCredentialSerializer(BaseValidateSerializer):
|
||||
class JiraCredentialSerializer(BaseValidateSerializer):
|
||||
user_mail = serializers.EmailField(required=True)
|
||||
api_token = serializers.CharField(required=True)
|
||||
domain = serializers.CharField(required=True)
|
||||
|
||||
class Meta:
|
||||
resource_name = "integrations"
|
||||
@@ -165,8 +167,12 @@ class JiraCredentialSerializer(BaseValidateSerializer):
|
||||
"description": "The API token for authentication with JIRA. This can be generated from your "
|
||||
"Atlassian account settings.",
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').",
|
||||
},
|
||||
},
|
||||
"required": ["user_mail", "api_token"],
|
||||
"required": ["user_mail", "api_token", "domain"],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -218,27 +224,10 @@ class IntegrationCredentialField(serializers.JSONField):
|
||||
{
|
||||
"type": "object",
|
||||
"title": "JIRA",
|
||||
"properties": {
|
||||
"project_key": {
|
||||
"type": "string",
|
||||
"description": "The JIRA project key where issues will be created (e.g., 'PROJ', 'SEC').",
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').",
|
||||
},
|
||||
"issue_types": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of JIRA issue types to create for findings.",
|
||||
},
|
||||
"issue_labels": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of labels to apply to created JIRA issues..",
|
||||
},
|
||||
},
|
||||
"required": ["project_key", "domain"],
|
||||
"description": "JIRA integration does not accept any configuration in the payload. Leave it as an "
|
||||
"empty JSON object (`{}`).",
|
||||
"properties": {},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1972,8 +1972,7 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer):
|
||||
integration_type == Integration.IntegrationChoices.JIRA
|
||||
and Integration.objects.filter(
|
||||
configuration__contains={
|
||||
"domain": attrs.get("configuration").get("domain"),
|
||||
"project_key": attrs.get("configuration").get("project_key"),
|
||||
"domain": attrs.get("configuration").get("domain")
|
||||
}
|
||||
).exists()
|
||||
):
|
||||
@@ -2038,7 +2037,28 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer):
|
||||
config_serializer = SecurityHubConfigSerializer
|
||||
credentials_serializers = [AWSCredentialSerializer]
|
||||
elif integration_type == Integration.IntegrationChoices.JIRA:
|
||||
if providers:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"providers": "Relationship field is not accepted. This integration applies to all providers."
|
||||
}
|
||||
)
|
||||
if configuration:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"configuration": "This integration does not support custom configuration."
|
||||
}
|
||||
)
|
||||
config_serializer = JiraConfigSerializer
|
||||
# Create non-editable configuration for JIRA integration
|
||||
default_jira_issue_types = ["Task", "Bug"]
|
||||
configuration.update(
|
||||
{
|
||||
"projects": {},
|
||||
"issue_types": default_jira_issue_types,
|
||||
"domain": credentials.get("domain"),
|
||||
}
|
||||
)
|
||||
credentials_serializers = [JiraCredentialSerializer]
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
@@ -2103,6 +2123,10 @@ class IntegrationSerializer(RLSSerializer):
|
||||
for provider in representation["providers"]
|
||||
if provider["id"] in allowed_provider_ids
|
||||
]
|
||||
if instance.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
representation["configuration"].update(
|
||||
{"domain": instance.credentials.get("domain")}
|
||||
)
|
||||
return representation
|
||||
|
||||
|
||||
@@ -2203,7 +2227,10 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
|
||||
def validate(self, attrs):
|
||||
integration_type = self.instance.integration_type
|
||||
providers = attrs.get("providers")
|
||||
configuration = attrs.get("configuration") or self.instance.configuration
|
||||
if integration_type != Integration.IntegrationChoices.JIRA:
|
||||
configuration = attrs.get("configuration") or self.instance.configuration
|
||||
else:
|
||||
configuration = attrs.get("configuration", {})
|
||||
credentials = attrs.get("credentials") or self.instance.credentials
|
||||
|
||||
self.validate_integration_data(
|
||||
@@ -2233,6 +2260,15 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
# Ensure JIRA integrations show updated domain in configuration from credentials
|
||||
if instance.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
representation["configuration"].update(
|
||||
{"domain": instance.credentials.get("domain")}
|
||||
)
|
||||
return representation
|
||||
|
||||
|
||||
# Processors
|
||||
|
||||
|
||||
Reference in New Issue
Block a user