mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(integrations): Support JIRA integration in the API (#8622)
This commit is contained in:
committed by
GitHub
parent
665c9d878a
commit
d4eb4bdca7
@@ -2,6 +2,13 @@
|
||||
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
## [1.12.0] (Prowler 5.11.0)
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.12.0"
|
||||
version = "1.13.0"
|
||||
|
||||
[project.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.12.0
|
||||
version: 1.13.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
@@ -9132,6 +9132,25 @@ components:
|
||||
default: false
|
||||
description: If true, archives findings that are not present 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
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -9171,6 +9190,24 @@ components:
|
||||
- User_Session-1
|
||||
- Test.Session@2
|
||||
pattern: ^[a-zA-Z0-9=,.@_-]+$
|
||||
- type: object
|
||||
title: JIRA Credentials
|
||||
properties:
|
||||
user_mail:
|
||||
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.
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
writeOnly: true
|
||||
required:
|
||||
- integration_type
|
||||
@@ -9292,6 +9329,25 @@ components:
|
||||
default: false
|
||||
description: If true, archives findings that are not present
|
||||
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
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -9332,6 +9388,24 @@ components:
|
||||
- User_Session-1
|
||||
- Test.Session@2
|
||||
pattern: ^[a-zA-Z0-9=,.@_-]+$
|
||||
- type: object
|
||||
title: JIRA Credentials
|
||||
properties:
|
||||
user_mail:
|
||||
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.
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
writeOnly: true
|
||||
required:
|
||||
- integration_type
|
||||
@@ -9468,6 +9542,25 @@ components:
|
||||
default: false
|
||||
description: If true, archives findings that are not present 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
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -9507,6 +9600,24 @@ components:
|
||||
- User_Session-1
|
||||
- Test.Session@2
|
||||
pattern: ^[a-zA-Z0-9=,.@_-]+$
|
||||
- type: object
|
||||
title: JIRA Credentials
|
||||
properties:
|
||||
user_mail:
|
||||
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.
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
writeOnly: true
|
||||
relationships:
|
||||
type: object
|
||||
@@ -10873,6 +10984,25 @@ components:
|
||||
default: false
|
||||
description: If true, archives findings that are not present
|
||||
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
|
||||
credentials:
|
||||
oneOf:
|
||||
- type: object
|
||||
@@ -10913,6 +11043,24 @@ components:
|
||||
- User_Session-1
|
||||
- Test.Session@2
|
||||
pattern: ^[a-zA-Z0-9=,.@_-]+$
|
||||
- type: object
|
||||
title: JIRA Credentials
|
||||
properties:
|
||||
user_mail:
|
||||
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.
|
||||
required:
|
||||
- user_mail
|
||||
- domain
|
||||
- api_token
|
||||
writeOnly: true
|
||||
relationships:
|
||||
type: object
|
||||
|
||||
@@ -502,3 +502,62 @@ class TestProwlerIntegrationConnectionTest:
|
||||
assert integration.configuration["regions"]["eu-west-1"] is True
|
||||
assert integration.configuration["regions"]["ap-south-1"] is False
|
||||
integration.save.assert_called_once()
|
||||
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_success_basic_auth(self, mock_jira_class):
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.credentials = {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test_api_token",
|
||||
}
|
||||
integration.configuration = {
|
||||
"domain": "example.atlassian.net",
|
||||
}
|
||||
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = True
|
||||
mock_connection.error = None
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is True
|
||||
assert result.error is None
|
||||
|
||||
mock_jira_class.test_connection.assert_called_once_with(
|
||||
user_mail="test@example.com",
|
||||
api_token="test_api_token",
|
||||
domain="example.atlassian.net",
|
||||
raise_on_exception=False,
|
||||
)
|
||||
|
||||
@patch("api.utils.Jira")
|
||||
def test_jira_connection_failure_invalid_credentials(self, mock_jira_class):
|
||||
integration = MagicMock()
|
||||
integration.integration_type = Integration.IntegrationChoices.JIRA
|
||||
integration.credentials = {
|
||||
"user_mail": "invalid@example.com",
|
||||
"api_token": "invalid_token",
|
||||
}
|
||||
integration.configuration = {
|
||||
"domain": "invalid.atlassian.net",
|
||||
}
|
||||
|
||||
# Mock failed JIRA connection
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.is_connected = False
|
||||
mock_connection.error = Exception("Authentication failed: Invalid credentials")
|
||||
mock_jira_class.test_connection.return_value = mock_connection
|
||||
|
||||
result = prowler_integration_connection_test(integration)
|
||||
|
||||
assert result.is_connected is False
|
||||
assert "Authentication failed: Invalid credentials" in str(result.error)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -5660,6 +5660,18 @@ 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(
|
||||
@@ -5806,6 +5818,40 @@ class TestIntegrationViewSet:
|
||||
"invalid",
|
||||
None,
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {
|
||||
"project_key": "JIRA",
|
||||
},
|
||||
"credentials": {},
|
||||
},
|
||||
"required",
|
||||
"domain",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {
|
||||
"project_key": "JIRA",
|
||||
"domain": "prowlerdomain",
|
||||
},
|
||||
},
|
||||
"required",
|
||||
"credentials",
|
||||
),
|
||||
(
|
||||
{
|
||||
"integration_type": "jira",
|
||||
"configuration": {
|
||||
"project_key": "JIRA",
|
||||
"domain": "prowlerdomain",
|
||||
},
|
||||
"credentials": {"api_token": "api-token"},
|
||||
},
|
||||
"invalid",
|
||||
"credentials",
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
@@ -5995,6 +6041,110 @@ class TestIntegrationViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_integrations_create_duplicate_amazon_s3(
|
||||
self, authenticated_client, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
|
||||
# Create first S3 integration
|
||||
data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.AMAZON_S3,
|
||||
"configuration": {
|
||||
"bucket_name": "test-bucket",
|
||||
"output_directory": "test-output",
|
||||
},
|
||||
"credentials": {
|
||||
"role_arn": "arn:aws:iam::123456789012:role/test-role",
|
||||
"external_id": "test-external-id",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
"relationships": {
|
||||
"providers": {
|
||||
"data": [{"type": "providers", "id": str(provider.id)}]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# First creation should succeed
|
||||
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
|
||||
|
||||
# Attempt to create duplicate should return 409
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_409_CONFLICT
|
||||
assert (
|
||||
"This integration already exists" in response.json()["errors"][0]["detail"]
|
||||
)
|
||||
assert (
|
||||
response.json()["errors"][0]["source"]["pointer"]
|
||||
== "/data/attributes/configuration"
|
||||
)
|
||||
|
||||
def test_integrations_create_duplicate_jira(
|
||||
self, authenticated_client, providers_fixture
|
||||
):
|
||||
provider = providers_fixture[0]
|
||||
|
||||
# Create first JIRA integration
|
||||
data = {
|
||||
"data": {
|
||||
"type": "integrations",
|
||||
"attributes": {
|
||||
"integration_type": Integration.IntegrationChoices.JIRA,
|
||||
"configuration": {
|
||||
"project_key": "TEST",
|
||||
"domain": "test.atlassian.net",
|
||||
},
|
||||
"credentials": {
|
||||
"user_mail": "test@example.com",
|
||||
"api_token": "test-api-token",
|
||||
},
|
||||
"enabled": True,
|
||||
},
|
||||
"relationships": {
|
||||
"providers": {
|
||||
"data": [{"type": "providers", "id": str(provider.id)}]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# First creation should succeed
|
||||
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
|
||||
|
||||
# Attempt to create duplicate should return 409
|
||||
response = authenticated_client.post(
|
||||
reverse("integration-list"),
|
||||
data=json.dumps(data),
|
||||
content_type="application/vnd.api+json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_409_CONFLICT
|
||||
assert (
|
||||
"This integration already exists" in response.json()["errors"][0]["detail"]
|
||||
)
|
||||
assert (
|
||||
response.json()["errors"][0]["source"]["pointer"]
|
||||
== "/data/attributes/configuration"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSAMLTokenValidation:
|
||||
|
||||
@@ -9,6 +9,7 @@ from api.db_router import MainRouter
|
||||
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.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
|
||||
@@ -199,7 +200,8 @@ def prowler_integration_connection_test(integration: Integration) -> Connection:
|
||||
raise_on_exception=False,
|
||||
)
|
||||
# TODO: It is possible that we can unify the connection test for all integrations, but need refactoring
|
||||
# to avoid code duplication. Actually the AWS integrations are similar, so SecurityHub and S3 can be unified making some changes in the SDK.
|
||||
# to avoid code duplication. Actually the AWS integrations are similar, so SecurityHub and S3 can be unified
|
||||
# making some changes in the SDK.
|
||||
elif (
|
||||
integration.integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB
|
||||
):
|
||||
@@ -236,7 +238,11 @@ def prowler_integration_connection_test(integration: Integration) -> Connection:
|
||||
|
||||
return connection
|
||||
elif integration.integration_type == Integration.IntegrationChoices.JIRA:
|
||||
pass
|
||||
return Jira.test_connection(
|
||||
**integration.credentials,
|
||||
domain=integration.configuration["domain"],
|
||||
raise_on_exception=False,
|
||||
)
|
||||
elif integration.integration_type == Integration.IntegrationChoices.SLACK:
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -67,6 +67,16 @@ class SecurityHubConfigSerializer(BaseValidateSerializer):
|
||||
resource_name = "integrations"
|
||||
|
||||
|
||||
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())
|
||||
|
||||
class Meta:
|
||||
resource_name = "integrations"
|
||||
|
||||
|
||||
class AWSCredentialSerializer(BaseValidateSerializer):
|
||||
role_arn = serializers.CharField(required=False)
|
||||
external_id = serializers.CharField(required=False)
|
||||
@@ -82,6 +92,14 @@ class AWSCredentialSerializer(BaseValidateSerializer):
|
||||
resource_name = "integrations"
|
||||
|
||||
|
||||
class JiraCredentialSerializer(BaseValidateSerializer):
|
||||
user_mail = serializers.EmailField(required=True)
|
||||
api_token = serializers.CharField(required=True)
|
||||
|
||||
class Meta:
|
||||
resource_name = "integrations"
|
||||
|
||||
|
||||
@extend_schema_field(
|
||||
{
|
||||
"oneOf": [
|
||||
@@ -133,6 +151,23 @@ class AWSCredentialSerializer(BaseValidateSerializer):
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"title": "JIRA Credentials",
|
||||
"properties": {
|
||||
"user_mail": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "The email address of the JIRA user account.",
|
||||
},
|
||||
"api_token": {
|
||||
"type": "string",
|
||||
"description": "The API token for authentication with JIRA. This can be generated from your "
|
||||
"Atlassian account settings.",
|
||||
},
|
||||
},
|
||||
"required": ["user_mail", "api_token"],
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
@@ -153,7 +188,10 @@ class IntegrationCredentialField(serializers.JSONField):
|
||||
},
|
||||
"output_directory": {
|
||||
"type": "string",
|
||||
"description": 'The directory path within the bucket where files will be saved. Optional - defaults to "output" if not provided. Path will be normalized to remove excessive slashes and invalid characters are not allowed (< > : " | ? *). Maximum length is 900 characters.',
|
||||
"description": "The directory path within the bucket where files will be saved. Optional - "
|
||||
'defaults to "output" if not provided. Path will be normalized to remove '
|
||||
'excessive slashes and invalid characters are not allowed (< > : " | ? *). '
|
||||
"Maximum length is 900 characters.",
|
||||
"maxLength": 900,
|
||||
"pattern": '^[^<>:"|?*]+$',
|
||||
"default": "output",
|
||||
@@ -177,6 +215,31 @@ 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"],
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ from rest_framework_simplejwt.exceptions import TokenError
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from api.exceptions import ConflictException
|
||||
from api.models import (
|
||||
Finding,
|
||||
Integration,
|
||||
@@ -45,6 +46,8 @@ from api.v1.serializer_utils.integrations import (
|
||||
AWSCredentialSerializer,
|
||||
IntegrationConfigField,
|
||||
IntegrationCredentialField,
|
||||
JiraConfigSerializer,
|
||||
JiraCredentialSerializer,
|
||||
S3ConfigSerializer,
|
||||
SecurityHubConfigSerializer,
|
||||
)
|
||||
@@ -1952,18 +1955,34 @@ class ScheduleDailyCreateSerializer(serializers.Serializer):
|
||||
|
||||
class BaseWriteIntegrationSerializer(BaseWriteSerializer):
|
||||
def validate(self, attrs):
|
||||
integration_type = attrs.get("integration_type")
|
||||
|
||||
if (
|
||||
attrs.get("integration_type") == Integration.IntegrationChoices.AMAZON_S3
|
||||
integration_type == Integration.IntegrationChoices.AMAZON_S3
|
||||
and Integration.objects.filter(
|
||||
configuration=attrs.get("configuration")
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"configuration": "This integration already exists."}
|
||||
raise ConflictException(
|
||||
detail="This integration already exists.",
|
||||
pointer="/data/attributes/configuration",
|
||||
)
|
||||
|
||||
if (
|
||||
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"),
|
||||
}
|
||||
).exists()
|
||||
):
|
||||
raise ConflictException(
|
||||
detail="This integration already exists.",
|
||||
pointer="/data/attributes/configuration",
|
||||
)
|
||||
|
||||
# Check if any provider already has a SecurityHub integration
|
||||
integration_type = attrs.get("integration_type")
|
||||
if hasattr(self, "instance") and self.instance and not integration_type:
|
||||
integration_type = self.instance.integration_type
|
||||
|
||||
@@ -1984,10 +2003,10 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer):
|
||||
query = query.exclude(integration=self.instance)
|
||||
|
||||
if query.exists():
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"providers": f"Provider {provider.id} already has a Security Hub integration. Only one Security Hub integration is allowed per provider."
|
||||
}
|
||||
raise ConflictException(
|
||||
detail=f"Provider {provider.id} already has a Security Hub integration. Only one "
|
||||
"Security Hub integration is allowed per provider.",
|
||||
pointer="/data/relationships/providers",
|
||||
)
|
||||
|
||||
return super().validate(attrs)
|
||||
@@ -2018,6 +2037,9 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer):
|
||||
)
|
||||
config_serializer = SecurityHubConfigSerializer
|
||||
credentials_serializers = [AWSCredentialSerializer]
|
||||
elif integration_type == Integration.IntegrationChoices.JIRA:
|
||||
config_serializer = JiraConfigSerializer
|
||||
credentials_serializers = [JiraCredentialSerializer]
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
@@ -2122,9 +2144,7 @@ class IntegrationCreateSerializer(BaseWriteIntegrationSerializer):
|
||||
and integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"providers": "At least one provider is required for the Security Hub integration."
|
||||
}
|
||||
{"providers": "At least one provider is required for this integration."}
|
||||
)
|
||||
|
||||
self.validate_integration_data(
|
||||
|
||||
@@ -293,7 +293,7 @@ class SchemaView(SpectacularAPIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
spectacular_settings.TITLE = "Prowler API"
|
||||
spectacular_settings.VERSION = "1.12.0"
|
||||
spectacular_settings.VERSION = "1.13.0"
|
||||
spectacular_settings.DESCRIPTION = (
|
||||
"Prowler API specification.\n\nThis file is auto-generated."
|
||||
)
|
||||
|
||||
@@ -330,7 +330,8 @@ def upload_security_hub_integration(
|
||||
|
||||
if not connected:
|
||||
logger.error(
|
||||
f"Security Hub connection failed for integration {integration.id}: {security_hub.error}"
|
||||
f"Security Hub connection failed for integration {integration.id}: "
|
||||
f"{security_hub.error}"
|
||||
)
|
||||
integration.connected = False
|
||||
integration.save()
|
||||
@@ -338,7 +339,8 @@ def upload_security_hub_integration(
|
||||
|
||||
security_hub_client = security_hub
|
||||
logger.info(
|
||||
f"Sending {'fail' if send_only_fails else 'all'} findings to Security Hub via integration {integration.id}"
|
||||
f"Sending {'fail' if send_only_fails else 'all'} findings to Security Hub via "
|
||||
f"integration {integration.id}"
|
||||
)
|
||||
else:
|
||||
# Update findings in existing client for this batch
|
||||
|
||||
Reference in New Issue
Block a user