diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 1e7bd1a12b..f8df5e6256 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -8909,14 +8909,15 @@ components: output_directory: type: string description: 'The directory path within the bucket where files - will be saved. Path will be normalized to remove excessive slashes - and invalid characters are not allowed (< > : " | ? *). Maximum - length is 900 characters.' + 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 required: - bucket_name - - output_directory credentials: oneOf: - type: object @@ -9054,14 +9055,15 @@ components: output_directory: type: string description: 'The directory path within the bucket where files - will be saved. Path will be normalized to remove excessive - slashes and invalid characters are not allowed (< > : " - | ? *). Maximum length is 900 characters.' + 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 required: - bucket_name - - output_directory credentials: oneOf: - type: object @@ -9214,14 +9216,15 @@ components: output_directory: type: string description: 'The directory path within the bucket where files - will be saved. Path will be normalized to remove excessive slashes - and invalid characters are not allowed (< > : " | ? *). Maximum - length is 900 characters.' + 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 required: - bucket_name - - output_directory credentials: oneOf: - type: object @@ -10572,14 +10575,15 @@ components: output_directory: type: string description: 'The directory path within the bucket where files - will be saved. Path will be normalized to remove excessive - slashes and invalid characters are not allowed (< > : " - | ? *). Maximum length is 900 characters.' + 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 required: - bucket_name - - output_directory credentials: oneOf: - type: object diff --git a/api/src/backend/api/tests/test_serializers.py b/api/src/backend/api/tests/test_serializers.py index 4c4b877495..a52b3464d8 100644 --- a/api/src/backend/api/tests/test_serializers.py +++ b/api/src/backend/api/tests/test_serializers.py @@ -30,9 +30,6 @@ class TestS3ConfigSerializer: """Test that empty values raise validation errors.""" serializer = S3ConfigSerializer() - with pytest.raises(ValidationError, match="Output directory cannot be empty"): - serializer.validate_output_directory("") - with pytest.raises( ValidationError, match="Output directory cannot be empty or just" ): diff --git a/api/src/backend/api/v1/serializer_utils/integrations.py b/api/src/backend/api/v1/serializer_utils/integrations.py index 49658a2042..1df0550655 100644 --- a/api/src/backend/api/v1/serializer_utils/integrations.py +++ b/api/src/backend/api/v1/serializer_utils/integrations.py @@ -9,15 +9,17 @@ from api.v1.serializer_utils.base import BaseValidateSerializer class S3ConfigSerializer(BaseValidateSerializer): bucket_name = serializers.CharField() - output_directory = serializers.CharField() + output_directory = serializers.CharField(allow_blank=True) def validate_output_directory(self, value): """ Validate the output_directory field to ensure it's a properly formatted path. Prevents paths with excessive slashes like "///////test". + If empty, sets a default value. """ + # If empty or None, set default value if not value: - raise serializers.ValidationError("Output directory cannot be empty.") + return "output" # Normalize the path to remove excessive slashes normalized_path = os.path.normpath(value) @@ -35,7 +37,7 @@ class S3ConfigSerializer(BaseValidateSerializer): # Check for empty path after normalization if not normalized_path or normalized_path == ".": raise serializers.ValidationError( - "Output directory cannot be empty or just '.'." + "Output directory cannot be empty or just '.' or '/'." ) # Check for paths that are too long (S3 key limit is 1024 characters, leave some room for filename) @@ -136,12 +138,13 @@ class IntegrationCredentialField(serializers.JSONField): }, "output_directory": { "type": "string", - "description": 'The directory path within the bucket where files will be saved. 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", }, }, - "required": ["bucket_name", "output_directory"], + "required": ["bucket_name"], }, ] } diff --git a/api/src/backend/tasks/jobs/integrations.py b/api/src/backend/tasks/jobs/integrations.py index f4b6b6cf58..1955f28bb1 100644 --- a/api/src/backend/tasks/jobs/integrations.py +++ b/api/src/backend/tasks/jobs/integrations.py @@ -83,13 +83,6 @@ def upload_s3_integration( for integration in integrations: try: connected, s3 = get_s3_client_from_integration(integration) - # Since many scans will be send to the same S3 bucket, we need to - # add the output directory to the S3 output directory to avoid - # overwriting the files and known the scan origin. - folder = os.getenv("OUTPUT_DIRECTORY", "/tmp/prowler_api_output") - s3._output_directory = ( - f"{s3._output_directory}{output_directory.split(folder)[-1]}" - ) except Exception as e: logger.error( f"S3 connection failed for integration {integration.id}: {e}" diff --git a/api/src/backend/tasks/tests/test_integrations.py b/api/src/backend/tasks/tests/test_integrations.py index 72f6d88365..8f047b304b 100644 --- a/api/src/backend/tasks/tests/test_integrations.py +++ b/api/src/backend/tasks/tests/test_integrations.py @@ -331,6 +331,35 @@ class TestS3IntegrationUploads: # Verify complex path normalization assert configuration["output_directory"] == "test/folder/subfolder" + @patch("tasks.jobs.integrations.S3") + def test_s3_client_uses_output_directory_in_object_paths(self, mock_s3_class): + """Test that S3 client uses output_directory correctly when generating object paths.""" + mock_integration = MagicMock() + mock_integration.credentials = { + "aws_access_key_id": "AKIA...", + "aws_secret_access_key": "SECRET", + } + mock_integration.configuration = { + "bucket_name": "test-bucket", + "output_directory": "my-custom-prefix/scan-results", + } + + mock_s3_instance = MagicMock() + mock_connection = MagicMock() + mock_connection.is_connected = True + mock_s3_instance.test_connection.return_value = mock_connection + mock_s3_class.return_value = mock_s3_instance + + connected, s3 = get_s3_client_from_integration(mock_integration) + + assert connected is True + # Verify S3 was initialized with the correct output_directory + mock_s3_class.assert_called_once_with( + **mock_integration.credentials, + bucket_name="test-bucket", + output_directory="my-custom-prefix/scan-results", + ) + @pytest.mark.django_db class TestProwlerIntegrationConnectionTest: