diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 3ce1c86ad..04115d95f 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -65,6 +65,14 @@ jobs: GS_BASE_64_ENCODED_PRIVATE_KEY_DATA: ${{ secrets.GS_BASE_64_ENCODED_PRIVATE_KEY_DATA }} GS_BUCKET: ${{ secrets.GS_BUCKET }} GS_PROJECT_ID: ${{ secrets.GS_PROJECT_ID }} + ## Malware Scanner (Basic Auth) + MALWARE_SCANNER_URL: ${{ secrets.MALWARE_SCANNER_URL }} + MALWARE_SCANNER_USERNAME: ${{ secrets.MALWARE_SCANNER_USERNAME }} + MALWARE_SCANNER_PASSWORD: ${{ secrets.MALWARE_SCANNER_PASSWORD }} + ## Malware Scanner (mTLS) + MALWARE_SCANNER_MTLS_URI: ${{ secrets.MALWARE_SCANNER_MTLS_URI }} + MALWARE_SCANNER_MTLS_CERTIFICATE: ${{ secrets.MALWARE_SCANNER_MTLS_CERTIFICATE }} + MALWARE_SCANNER_MTLS_KEY: ${{ secrets.MALWARE_SCANNER_MTLS_KEY }} strategy: fail-fast: false matrix: diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java index 9f5d72e93..6fc0c25b8 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/configuration/Registration.java @@ -24,7 +24,9 @@ import com.sap.cds.feature.attachments.handler.draftservice.DraftPatchAttachmentsHandler; import com.sap.cds.feature.attachments.service.AttachmentService; import com.sap.cds.feature.attachments.service.AttachmentsServiceImpl; +import com.sap.cds.feature.attachments.service.MalwareScannerServiceImpl; import com.sap.cds.feature.attachments.service.handler.DefaultAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.service.handler.DefaultMalwareScannerServiceHandler; import com.sap.cds.feature.attachments.service.handler.transaction.EndTransactionMalwareScanProvider; import com.sap.cds.feature.attachments.service.handler.transaction.EndTransactionMalwareScanRunner; import com.sap.cds.feature.attachments.service.malware.AttachmentMalwareScanner; @@ -88,6 +90,7 @@ public void environment(CdsRuntimeConfigurer configurer) { @Override public void services(CdsRuntimeConfigurer configurer) { configurer.service(new AttachmentsServiceImpl()); + configurer.service(new MalwareScannerServiceImpl()); } @Override @@ -137,6 +140,9 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { configurer.eventHandler( new DefaultAttachmentsServiceHandler(malwareScanEndTransactionListener)); + // register event handler for malware scanner service + configurer.eventHandler(new DefaultMalwareScannerServiceHandler(scanClient)); + MarkAsDeletedAttachmentEvent deleteEvent = new MarkAsDeletedAttachmentEvent(outboxedAttachmentService); ModifyAttachmentEventFactory eventFactory = diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java new file mode 100644 index 000000000..a06078446 --- /dev/null +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerService.java @@ -0,0 +1,31 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service; + +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.services.Service; +import java.io.InputStream; + +/** + * The {@link MalwareScannerService} provides malware scanning capabilities independent of the + * {@link AttachmentService}. It can be injected from the CAP service catalog and used to scan + * arbitrary content for malware. + */ +public interface MalwareScannerService extends Service { + + /** The {@link MalwareScannerService} uses this name in the service catalog */ + String DEFAULT_NAME = "MalwareScannerService$Default"; + + /** This event is emitted when content shall be scanned for malware */ + String EVENT_SCAN_CONTENT = "SCAN_CONTENT"; + + /** + * Scans the given content for malware. + * + * @param content the {@link InputStream} of the content to scan + * @return the result of the malware scan + * @see MalwareScanResultStatus + */ + MalwareScanResultStatus scanContent(InputStream content); +} diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java new file mode 100644 index 000000000..9854daf4d --- /dev/null +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImpl.java @@ -0,0 +1,31 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service; + +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.feature.attachments.service.model.servicehandler.MalwareScanEventContext; +import com.sap.cds.services.ServiceDelegator; +import java.io.InputStream; + +/** + * Implementation of the {@link MalwareScannerService} interface. The main purpose of this class is + * to set data in the corresponding context and to call the emit method for the + * MalwareScannerService. + */ +public class MalwareScannerServiceImpl extends ServiceDelegator implements MalwareScannerService { + + public MalwareScannerServiceImpl() { + super(DEFAULT_NAME); + } + + @Override + public MalwareScanResultStatus scanContent(InputStream content) { + var scanContext = MalwareScanEventContext.create(); + scanContext.setContent(content); + + emit(scanContext); + + return scanContext.getScanResult(); + } +} diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java new file mode 100644 index 000000000..97d038a34 --- /dev/null +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandler.java @@ -0,0 +1,63 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service.handler; + +import com.sap.cds.feature.attachments.service.MalwareScannerService; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanClient; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.feature.attachments.service.model.servicehandler.MalwareScanEventContext; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.HandlerOrder; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The class {@link DefaultMalwareScannerServiceHandler} is the default event handler for the {@link + * MalwareScannerService}. It delegates to the {@link MalwareScanClient} to perform the actual + * malware scan. + */ +@ServiceName(value = "*", type = MalwareScannerService.class) +public class DefaultMalwareScannerServiceHandler implements EventHandler { + + private static final int DEFAULT_ON = 10 * HandlerOrder.AFTER + HandlerOrder.LATE; + + private static final Logger logger = + LoggerFactory.getLogger(DefaultMalwareScannerServiceHandler.class); + + private final MalwareScanClient malwareScanClient; + + /** + * Constructs a new instance of {@link DefaultMalwareScannerServiceHandler}. + * + * @param malwareScanClient an optional {@link MalwareScanClient} instance, may be {@code null} if + * no malware scanner service binding is available + */ + public DefaultMalwareScannerServiceHandler(MalwareScanClient malwareScanClient) { + this.malwareScanClient = malwareScanClient; + } + + @On + @HandlerOrder(DEFAULT_ON) + void scanContent(MalwareScanEventContext context) { + if (malwareScanClient == null) { + logger.info("No malware scanner service binding available. Returning NO_SCANNER status."); + context.setScanResult(MalwareScanResultStatus.NO_SCANNER); + context.setCompleted(); + return; + } + + try { + logger.debug("Starting malware scan of content."); + MalwareScanResultStatus result = malwareScanClient.scanContent(context.getContent()); + context.setScanResult(result); + logger.debug("Malware scan completed with status '{}'.", result); + } catch (Exception e) { + logger.error("Error during malware scan.", e); + context.setScanResult(MalwareScanResultStatus.FAILED); + } + context.setCompleted(); + } +} diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java new file mode 100644 index 000000000..898efbe1a --- /dev/null +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContext.java @@ -0,0 +1,49 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service.model.servicehandler; + +import com.sap.cds.feature.attachments.service.MalwareScannerService; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.services.EventContext; +import com.sap.cds.services.EventName; +import java.io.InputStream; + +/** The {@link MalwareScanEventContext} is used to store the context of a malware scan event. */ +@EventName(MalwareScannerService.EVENT_SCAN_CONTENT) +public interface MalwareScanEventContext extends EventContext { + + /** + * Creates an {@link EventContext} already overlaid with this interface. The event is set to be + * {@link MalwareScannerService#EVENT_SCAN_CONTENT} + * + * @return the {@link MalwareScanEventContext} + */ + static MalwareScanEventContext create() { + return EventContext.create(MalwareScanEventContext.class, null); + } + + /** + * @return the content {@link InputStream} to scan for malware + */ + InputStream getContent(); + + /** + * Sets the content to scan for malware + * + * @param content the content {@link InputStream} to scan + */ + void setContent(InputStream content); + + /** + * @return the result of the malware scan or {@code null} if not yet scanned + */ + MalwareScanResultStatus getScanResult(); + + /** + * Sets the result of the malware scan + * + * @param scanResult the result of the malware scan + */ + void setScanResult(MalwareScanResultStatus scanResult); +} diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java index 987b2b4c1..c1370d5cf 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java @@ -19,7 +19,9 @@ import com.sap.cds.feature.attachments.handler.draftservice.DraftCancelAttachmentsHandler; import com.sap.cds.feature.attachments.handler.draftservice.DraftPatchAttachmentsHandler; import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.MalwareScannerService; import com.sap.cds.feature.attachments.service.handler.DefaultAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.service.handler.DefaultMalwareScannerServiceHandler; import com.sap.cds.feature.attachments.service.malware.DefaultAttachmentMalwareScanner; import com.sap.cds.services.Service; import com.sap.cds.services.ServiceCatalog; @@ -96,13 +98,16 @@ void setup() { void serviceIsRegistered() { cut.services(configurer); - verify(configurer).service(serviceArgumentCaptor.capture()); + verify(configurer, times(2)).service(serviceArgumentCaptor.capture()); var services = serviceArgumentCaptor.getAllValues(); - assertThat(services).hasSize(1); + assertThat(services).hasSize(2); var attachmentServiceFound = services.stream().anyMatch(AttachmentService.class::isInstance); + var malwareScannerServiceFound = + services.stream().anyMatch(MalwareScannerService.class::isInstance); assertThat(attachmentServiceFound).isTrue(); + assertThat(malwareScannerServiceFound).isTrue(); } @Test @@ -119,7 +124,7 @@ void handlersAreRegistered() { cut.eventHandlers(configurer); - var handlerSize = 8; + var handlerSize = 9; verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture()); checkHandlers(handlerArgumentCaptor.getAllValues(), handlerSize); } @@ -139,7 +144,7 @@ void handlersAreRegisteredWithoutOutboxService() { cut.eventHandlers(configurer); - var handlerSize = 8; + var handlerSize = 9; verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture()); checkHandlers(handlerArgumentCaptor.getAllValues(), handlerSize); } @@ -147,6 +152,7 @@ void handlersAreRegisteredWithoutOutboxService() { private void checkHandlers(List handlers, int handlerSize) { assertThat(handlers).hasSize(handlerSize); isHandlerForClassIncluded(handlers, DefaultAttachmentsServiceHandler.class); + isHandlerForClassIncluded(handlers, DefaultMalwareScannerServiceHandler.class); isHandlerForClassIncluded(handlers, CreateAttachmentsHandler.class); isHandlerForClassIncluded(handlers, UpdateAttachmentsHandler.class); isHandlerForClassIncluded(handlers, DeleteAttachmentsHandler.class); @@ -167,11 +173,12 @@ void lessHandlersAreRegistered() { cut.eventHandlers(configurer); - var handlerSize = 1; + var handlerSize = 2; verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture()); var handlers = handlerArgumentCaptor.getAllValues(); assertThat(handlers).hasSize(handlerSize); isHandlerForClassIncluded(handlers, DefaultAttachmentsServiceHandler.class); + isHandlerForClassIncluded(handlers, DefaultMalwareScannerServiceHandler.class); // event handlers for application services are not registered isHandlerForClassMissing(handlers, CreateAttachmentsHandler.class); isHandlerForClassMissing(handlers, UpdateAttachmentsHandler.class); diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java new file mode 100644 index 000000000..723daf512 --- /dev/null +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/MalwareScannerServiceImplTest.java @@ -0,0 +1,67 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.feature.attachments.service.model.servicehandler.MalwareScanEventContext; +import com.sap.cds.services.environment.CdsEnvironment; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.handler.Handler; +import com.sap.cds.services.impl.ServiceSPI; +import com.sap.cds.services.runtime.CdsRuntime; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class MalwareScannerServiceImplTest { + + private MalwareScannerServiceImpl cut; + private Handler handler; + private ServiceSPI serviceSpi; + + @BeforeEach + void setup() { + cut = new MalwareScannerServiceImpl(); + + CdsEnvironment env = mock(CdsEnvironment.class); + CdsProperties props = new CdsProperties(); + when(env.getCdsProperties()).thenReturn(props); + CdsRuntime runtime = mock(CdsRuntime.class); + when(runtime.getEnvironment()).thenReturn(env); + handler = mock(Handler.class); + serviceSpi = (ServiceSPI) cut.getDelegatedService(); + serviceSpi.setCdsRuntime(runtime); + } + + @ParameterizedTest + @EnumSource(MalwareScanResultStatus.class) + void scanContentReturnsCorrectStatus(MalwareScanResultStatus expectedStatus) { + var contextReference = new AtomicReference(); + var stream = mock(InputStream.class); + doAnswer( + input -> { + var context = (MalwareScanEventContext) input.getArgument(0); + contextReference.set(context); + context.setCompleted(); + context.setScanResult(expectedStatus); + return null; + }) + .when(handler) + .process(any()); + serviceSpi.on(MalwareScannerService.EVENT_SCAN_CONTENT, "", handler); + + var result = cut.scanContent(stream); + + assertThat(result).isEqualTo(expectedStatus); + assertThat(contextReference.get().getContent()).isEqualTo(stream); + } +} diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java new file mode 100644 index 000000000..2fd99ca6b --- /dev/null +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/handler/DefaultMalwareScannerServiceHandlerTest.java @@ -0,0 +1,93 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service.handler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sap.cds.feature.attachments.service.MalwareScannerService; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanClient; +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import com.sap.cds.feature.attachments.service.model.servicehandler.MalwareScanEventContext; +import com.sap.cds.services.handler.annotations.HandlerOrder; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import java.io.InputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class DefaultMalwareScannerServiceHandlerTest { + + private static final int EXPECTED_HANDLER_ORDER = 11000; + + private DefaultMalwareScannerServiceHandler cut; + private MalwareScanClient malwareScanClient; + + @BeforeEach + void setup() { + malwareScanClient = mock(MalwareScanClient.class); + cut = new DefaultMalwareScannerServiceHandler(malwareScanClient); + } + + @ParameterizedTest + @EnumSource( + value = MalwareScanResultStatus.class, + names = {"CLEAN", "INFECTED", "ENCRYPTED"}) + void scanContentWithClientReturnsStatus(MalwareScanResultStatus expectedStatus) { + when(malwareScanClient.scanContent(any())).thenReturn(expectedStatus); + var context = MalwareScanEventContext.create(); + context.setContent(mock(InputStream.class)); + + cut.scanContent(context); + + assertThat(context.isCompleted()).isTrue(); + assertThat(context.getScanResult()).isEqualTo(expectedStatus); + } + + @Test + void scanContentWithoutClientReturnsNoScanner() { + cut = new DefaultMalwareScannerServiceHandler(null); + var context = MalwareScanEventContext.create(); + context.setContent(mock(InputStream.class)); + + cut.scanContent(context); + + assertThat(context.isCompleted()).isTrue(); + assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.NO_SCANNER); + } + + @Test + void scanContentWithClientThrowingExceptionReturnsFailed() { + when(malwareScanClient.scanContent(any())).thenThrow(new RuntimeException("scan error")); + var context = MalwareScanEventContext.create(); + context.setContent(mock(InputStream.class)); + + cut.scanContent(context); + + assertThat(context.isCompleted()).isTrue(); + assertThat(context.getScanResult()).isEqualTo(MalwareScanResultStatus.FAILED); + } + + @Test + void classHasCorrectAnnotation() { + var annotation = cut.getClass().getAnnotation(ServiceName.class); + + assertThat(annotation.value()).containsOnly("*"); + assertThat(annotation.type()).containsOnly(MalwareScannerService.class); + } + + @Test + void scanMethodHasCorrectAnnotation() throws NoSuchMethodException { + var scanMethod = cut.getClass().getDeclaredMethod("scanContent", MalwareScanEventContext.class); + var onAnnotation = scanMethod.getAnnotation(On.class); + var handlerOrderAnnotation = scanMethod.getAnnotation(HandlerOrder.class); + + assertThat(onAnnotation.event()).isEmpty(); + assertThat(handlerOrderAnnotation.value()).isEqualTo(EXPECTED_HANDLER_ORDER); + } +} diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScanClientIT.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScanClientIT.java new file mode 100644 index 000000000..e29302c74 --- /dev/null +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/malware/client/MalwareScanClientIT.java @@ -0,0 +1,133 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service.malware.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sap.cds.services.environment.CdsProperties.ConnectionPool; +import com.sap.cloud.environment.servicebinding.api.ServiceBinding; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +class MalwareScanClientIT { + // The tests in this class are intended to run against a real Malware Scanner instance. + // They require valid credentials set up in the environment. + // Basic auth and mTLS tests skip independently when their credentials are missing. + + private static final String EICAR_TEST_STRING = + "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; + + private static final ConnectionPool CONNECTION_POOL = + new ConnectionPool(Duration.ofSeconds(120), 20, 20); + + private static MalwareScanClient buildClient(Map creds) { + ServiceBinding binding = mock(ServiceBinding.class); + when(binding.getCredentials()).thenReturn(creds); + HttpClientProvider clientProvider = new MalwareScanClientProvider(binding, CONNECTION_POOL); + return new DefaultMalwareScanClient(clientProvider); + } + + private static String normalizePem(String pem) { + return pem.replace("\\n", "\n"); + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class BasicAuth { + + private MalwareScanClient client; + + @BeforeAll + void setUp() { + String url = System.getenv("MALWARE_SCANNER_URL"); + String username = System.getenv("MALWARE_SCANNER_USERNAME"); + String password = System.getenv("MALWARE_SCANNER_PASSWORD"); + + if (url == null || username == null || password == null) { + client = null; + } else { + client = buildClient(Map.of("url", url, "username", username, "password", password)); + } + + Assumptions.assumeTrue( + client != null, "Basic auth malware scanner credentials not available — skipping tests"); + } + + @Test + void scanCleanContent() { + MalwareScanResultStatus result = + client.scanContent( + new ByteArrayInputStream("safe content".getBytes(StandardCharsets.UTF_8))); + + assertThat(result).isEqualTo(MalwareScanResultStatus.CLEAN); + } + + @Test + void scanEicarTestFile() { + MalwareScanResultStatus result = + client.scanContent( + new ByteArrayInputStream(EICAR_TEST_STRING.getBytes(StandardCharsets.UTF_8))); + + assertThat(result).isEqualTo(MalwareScanResultStatus.INFECTED); + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Mtls { + + private MalwareScanClient client; + + @BeforeAll + void setUp() { + String uri = System.getenv("MALWARE_SCANNER_MTLS_URI"); + String certificate = System.getenv("MALWARE_SCANNER_MTLS_CERTIFICATE"); + String key = System.getenv("MALWARE_SCANNER_MTLS_KEY"); + + if (uri == null || certificate == null || key == null) { + client = null; + } else { + client = + buildClient( + Map.of( + "uri", + uri, + "certificate", + normalizePem(certificate), + "key", + normalizePem(key))); + } + + Assumptions.assumeTrue( + client != null, "mTLS malware scanner credentials not available — skipping tests"); + } + + @Test + void scanCleanContent() { + MalwareScanResultStatus result = + client.scanContent( + new ByteArrayInputStream("safe content".getBytes(StandardCharsets.UTF_8))); + + assertThat(result).isEqualTo(MalwareScanResultStatus.CLEAN); + } + + @Test + void scanEicarTestFile() { + MalwareScanResultStatus result = + client.scanContent( + new ByteArrayInputStream(EICAR_TEST_STRING.getBytes(StandardCharsets.UTF_8))); + + assertThat(result).isEqualTo(MalwareScanResultStatus.INFECTED); + } + } +} diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java new file mode 100644 index 000000000..8271aa8d0 --- /dev/null +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/service/model/servicehandler/MalwareScanEventContextTest.java @@ -0,0 +1,33 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.service.model.servicehandler; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.feature.attachments.service.malware.client.MalwareScanResultStatus; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class MalwareScanEventContextTest { + private MalwareScanEventContext cut; + + @BeforeEach + void setup() { + cut = MalwareScanEventContext.create(); + } + + @Test + void fieldsCanBeSetAndRead() throws IOException { + try (var testStream = new ByteArrayInputStream("testString".getBytes(StandardCharsets.UTF_8))) { + cut.setContent(testStream); + cut.setScanResult(MalwareScanResultStatus.CLEAN); + + assertThat(cut.getContent()).isEqualTo(testStream); + assertThat(cut.getScanResult()).isEqualTo(MalwareScanResultStatus.CLEAN); + } + } +}