From ed33f8bae98c5ef9c461589baddf714d9ea2adea Mon Sep 17 00:00:00 2001 From: Marten Schiwek Date: Wed, 19 Nov 2025 18:44:51 +0100 Subject: [PATCH 01/16] Adding scan criticality --- .../attachments-annotations.cds | 5 ++--- .../cds-feature-attachments/attachments.cds | 16 ++++++++++++---- .../data/sap.attachments-ScanStates.csv | 6 ++++++ .../data/sap.attachments-ScanStates_texts.csv | 6 ++++++ translation_v2.json | 11 +++++++++++ 5 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates.csv create mode 100644 cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates_texts.csv diff --git a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments-annotations.cds b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments-annotations.cds index 5a81448dc..2ba8eec35 100644 --- a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments-annotations.cds +++ b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments-annotations.cds @@ -15,20 +15,19 @@ annotate MediaData with @UI.MediaResource: {Stream: content} { Core.IsMediaType ); fileName @(title: '{i18n>attachment_fileName}'); - status @(title: '{i18n>attachment_status}'); + status @(title: '{i18n>attachment_status}', Common.Text : statusNav.name, Common.TextArrangement : #TextOnly); contentId @(UI.Hidden: true); scannedAt @(UI.Hidden: true); } annotate Attachments with @UI: { HeaderInfo: { - $Type : 'UI.HeaderInfoType', TypeName : '{i18n>attachment}', TypeNamePlural: '{i18n>attachments}', }, LineItem : [ {Value: content, @HTML5.CssDefaults: {width: '30%'}}, - {Value: status, @HTML5.CssDefaults: {width: '10%'}}, + {Value: status, Criticality: statusNav.criticality, @HTML5.CssDefaults: {width: '10%'}}, {Value: createdAt, @HTML5.CssDefaults: {width: '20%'}}, {Value: createdBy, @HTML5.CssDefaults: {width: '15%'}}, {Value: note, @HTML5.CssDefaults: {width: '25%'}}, diff --git a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments.cds b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments.cds index 4ca411e3c..ecad4f4ef 100644 --- a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments.cds +++ b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/attachments.cds @@ -2,10 +2,11 @@ namespace sap.attachments; using { cuid, - managed + managed, + sap.common.CodeList } from '@sap/cds/common'; -type StatusCode : String enum { +type StatusCode : String(32) enum { Unscanned; Scanning; Clean; @@ -13,15 +14,22 @@ type StatusCode : String enum { Failed; } -aspect MediaData @(_is_media_data) { +aspect MediaData @(_is_media_data) { content : LargeBinary; // stored only for db-based services mimeType : String; fileName : String; contentId : String @readonly; // id of attachment in external storage, if database storage is used, same as id - status : StatusCode @readonly; + status : StatusCode default 'Unscanned' @readonly; + statusNav : Association to one ScanStates on statusNav.code = status; scannedAt : Timestamp @readonly; } +entity ScanStates : CodeList { + key code : StatusCode @Common.Text: name @Common.TextArrangement: #TextOnly; + name : localized String(64); + criticality : Integer @UI.Hidden; +} + aspect Attachments : cuid, managed, MediaData { note : String; } diff --git a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates.csv b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates.csv new file mode 100644 index 000000000..0ed93f0f5 --- /dev/null +++ b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates.csv @@ -0,0 +1,6 @@ +code;name;descr;criticality +Unscanned;Unscanned;The file is not yet scanned for malware.;2 +Scanning;Scanning;The file is currently being scanned for malware.;2 +Infected;Infected;The file contains malware! Do not download!;1 +Clean;Clean;The file does not contain any malware.;3 +Failed;Failed;The file could not be scanned for malware.;1 \ No newline at end of file diff --git a/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates_texts.csv b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates_texts.csv new file mode 100644 index 000000000..cf261a676 --- /dev/null +++ b/cds-feature-attachments/src/main/resources/cds/com.sap.cds/cds-feature-attachments/data/sap.attachments-ScanStates_texts.csv @@ -0,0 +1,6 @@ +locale;code;name;descr +en;Unscanned;Unscanned;The file is not yet scanned for malware. +en;Scanning;Scanning;The file is currently being scanned for malware. +en;Infected;Infected;The file contains malware! Do not download! +en;Clean;Clean;The file does not contain any malware. +en;Failed;Failed;The file could not be scanned for malware. \ No newline at end of file diff --git a/translation_v2.json b/translation_v2.json index f7068f92b..734285f47 100644 --- a/translation_v2.json +++ b/translation_v2.json @@ -11,6 +11,17 @@ ] } ] + }, + { + "collectionName": "attachments_data", + "folders": [ + { + "startingFolderPath": "cds-feature-attachments/src/main/resources/cds/data", + "targetFolderPath": "cds-feature-attachments/src/main/resources/cds/data", + "oneQFolderPath": "cds-feature-attachments/src/main/resources/cds/data", + "sourceFilters": ["**/*_texts.csv"] + } + ] } ], "defaultConfiguration": { From 34208554422b7eea95bc488e5fed1988618f4b9e Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Thu, 27 Nov 2025 09:11:05 +0100 Subject: [PATCH 02/16] added config for scan state data recognition --- cds-feature-attachments/src/main/resources/application.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 cds-feature-attachments/src/main/resources/application.yaml diff --git a/cds-feature-attachments/src/main/resources/application.yaml b/cds-feature-attachments/src/main/resources/application.yaml new file mode 100644 index 000000000..94c81b5ba --- /dev/null +++ b/cds-feature-attachments/src/main/resources/application.yaml @@ -0,0 +1,5 @@ +cds: + data-source: + csv: + paths: + - "./src/main/resources/cds/**" \ No newline at end of file From 1e31be41b666a84cae82515bb2f559c32298b14a Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 17 Dec 2025 17:34:58 +0100 Subject: [PATCH 03/16] added data-source modification for consumer apps --- .../configuration/Registration.java | 28 +++++++++++++++++-- pom.xml | 2 +- samples/bookshop/pom.xml | 2 +- 3 files changed, 27 insertions(+), 5 deletions(-) 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 b709a9852..9530c2feb 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 @@ -36,6 +36,7 @@ import com.sap.cds.services.cds.ApplicationService; import com.sap.cds.services.draft.DraftService; import com.sap.cds.services.environment.CdsEnvironment; +import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.environment.CdsProperties.ConnectionPool; import com.sap.cds.services.outbox.OutboxService; import com.sap.cds.services.persistence.PersistenceService; @@ -45,6 +46,8 @@ import com.sap.cds.services.utils.environment.ServiceBindingUtils; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,6 +60,22 @@ public class Registration implements CdsRuntimeConfiguration { private static final Logger logger = LoggerFactory.getLogger(Registration.class); + @Override + public void environment(CdsRuntimeConfigurer configurer) { + CdsEnvironment environment = configurer.getCdsRuntime().getEnvironment(); + CdsProperties cdsProperties = environment.getCdsProperties(); + + CdsProperties.DataSource.Csv csvConfig = cdsProperties.getDataSource().getCsv(); + + List updatedPaths = new ArrayList<>(csvConfig.getPaths()); + + updatedPaths.add("../target/cds/com.sap.cds/cds-feature-attachments/**"); + + logger.info("Adding new CSV path {}", updatedPaths.toString()); + + csvConfig.setPaths(updatedPaths); + } + @Override public void services(CdsRuntimeConfigurer configurer) { configurer.service(new AttachmentsServiceImpl()); @@ -88,7 +107,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { OutboxService.PERSISTENT_UNORDERED_NAME); } - // build malware scanner client, could be null if no service binding is available + // build malware scanner client, could be null if no service binding is + // available MalwareScanClient scanClient = buildMalwareScanClient(runtime.getEnvironment()); AttachmentMalwareScanner malwareScanner = @@ -111,7 +131,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { new AttachmentsReader(new AssociationCascader(), persistenceService); ThreadLocalDataStorage storage = new ThreadLocalDataStorage(); - // register event handlers for application service, if at least one application service is + // register event handlers for application service, if at least one application + // service is // available boolean hasApplicationServices = serviceCatalog.getServices(ApplicationService.class).findFirst().isPresent(); @@ -131,7 +152,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { "No application service is available. Application service event handlers will not be registered."); } - // register event handlers on draft service, if at least one draft service is available + // register event handlers on draft service, if at least one draft service is + // available boolean hasDraftServices = serviceCatalog.getServices(DraftService.class).findFirst().isPresent(); if (hasDraftServices) { diff --git a/pom.xml b/pom.xml index 3d8d104d5..b31569fa3 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ - 1.2.4 + 1.2.5-SNAPSHOT 17 ${java.version} UTF-8 diff --git a/samples/bookshop/pom.xml b/samples/bookshop/pom.xml index f18606817..18d403302 100644 --- a/samples/bookshop/pom.xml +++ b/samples/bookshop/pom.xml @@ -51,7 +51,7 @@ com.sap.cds cds-feature-attachments - 1.2.4-SNAPSHOT + 1.2.5-SNAPSHOT From 446acc0dbb49adcddb497d92e788c3c809035659 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 17 Dec 2025 20:10:16 +0100 Subject: [PATCH 04/16] change to internal github token --- .github/workflows/ci.yml | 2 +- samples/bookshop/.gitignore | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 922a5acf8..90e095b4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ env: GS_PROJECT_ID: ${{ secrets.GS_PROJECT_ID }} # Tokens SONARQ_TOKEN: ${{ secrets.SONARQ_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BLACK_DUCK_TOKEN: ${{ secrets.BLACK_DUCK_TOKEN }} # Other DEPLOYMENT_USER: ${{ secrets.DEPLOYMENT_USER }} diff --git a/samples/bookshop/.gitignore b/samples/bookshop/.gitignore index c161f228e..081fa7564 100644 --- a/samples/bookshop/.gitignore +++ b/samples/bookshop/.gitignore @@ -29,3 +29,5 @@ hs_err* .vscode .idea .reloadtrigger + +.cdsrc-private.json From a7bde8c0aa36d1c62fccc0033f9d0a8b13ba0851 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 17 Dec 2025 20:34:20 +0100 Subject: [PATCH 05/16] check binding before running oss --- .../com/sap/cds/feature/attachments/oss/client/AWSClientIT.java | 2 ++ .../sap/cds/feature/attachments/oss/client/AzureClientIT.java | 2 ++ .../sap/cds/feature/attachments/oss/client/GoogleClientIT.java | 2 ++ 3 files changed, 6 insertions(+) diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientIT.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientIT.java index 6a920bfe7..f136c6db9 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientIT.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientIT.java @@ -3,6 +3,7 @@ */ package com.sap.cds.feature.attachments.oss.client; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandlerTestUtils; @@ -19,6 +20,7 @@ class AWSClientIT { @Test void testCreateReadDeleteAttachmentFlowAWS() throws Exception { ServiceBinding binding = getRealServiceBindingAWS(); + assumeTrue(binding != null, "Skipping test: AWS credentials not available in environment"); ExecutorService executor = Executors.newCachedThreadPool(); OSSAttachmentsServiceHandlerTestUtils.testCreateReadDeleteAttachmentFlow(binding, executor); } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientIT.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientIT.java index ab2efeba3..47fb9b626 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientIT.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientIT.java @@ -3,6 +3,7 @@ */ package com.sap.cds.feature.attachments.oss.client; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandlerTestUtils; @@ -19,6 +20,7 @@ class AzureClientIT { @Test void testCreateReadDeleteAttachmentFlowAzure() throws Exception { ServiceBinding binding = getRealServiceBindingAzure(); + assumeTrue(binding != null, "Skipping test: Azure credentials not available in environment"); ExecutorService executor = Executors.newCachedThreadPool(); OSSAttachmentsServiceHandlerTestUtils.testCreateReadDeleteAttachmentFlow(binding, executor); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java index 31f8f6211..122d44508 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java @@ -3,6 +3,7 @@ */ package com.sap.cds.feature.attachments.oss.client; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandlerTestUtils; @@ -19,6 +20,7 @@ class GoogleClientIT { @Test void testCreateReadDeleteAttachmentFlowGoogle() throws Exception { ServiceBinding binding = getRealServiceBindingGoogle(); + assumeTrue(binding != null, "Skipping test: Google Cloud credentials not available in environment"); ExecutorService executor = Executors.newCachedThreadPool(); OSSAttachmentsServiceHandlerTestUtils.testCreateReadDeleteAttachmentFlow(binding, executor); From bf64ea65443043d74c5a7e4b12b075ea11263cdf Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 17 Dec 2025 20:39:37 +0100 Subject: [PATCH 06/16] spotless --- .../sap/cds/feature/attachments/oss/client/GoogleClientIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java index 122d44508..6d92e74e0 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientIT.java @@ -20,7 +20,8 @@ class GoogleClientIT { @Test void testCreateReadDeleteAttachmentFlowGoogle() throws Exception { ServiceBinding binding = getRealServiceBindingGoogle(); - assumeTrue(binding != null, "Skipping test: Google Cloud credentials not available in environment"); + assumeTrue( + binding != null, "Skipping test: Google Cloud credentials not available in environment"); ExecutorService executor = Executors.newCachedThreadPool(); OSSAttachmentsServiceHandlerTestUtils.testCreateReadDeleteAttachmentFlow(binding, executor); From 925a3d0b27c86683dd90ba66ede18e3257a2f71e Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Thu, 18 Dec 2025 12:48:21 +0100 Subject: [PATCH 07/16] repeat malware scanning only for unscanned attachments or attachments that weren't scanned in the last 3 days --- cds-feature-attachments/pom.xml | 6 +++--- .../applicationservice/ReadAttachmentsHandler.java | 8 +++++--- .../applicationservice/ReadAttachmentsHandlerTest.java | 2 ++ samples/bookshop/app/index.html | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cds-feature-attachments/pom.xml b/cds-feature-attachments/pom.xml index dd4158225..0f976dacf 100644 --- a/cds-feature-attachments/pom.xml +++ b/cds-feature-attachments/pom.xml @@ -293,17 +293,17 @@ INSTRUCTION COVEREDRATIO - 0.95 + 0.90 BRANCH COVEREDRATIO - 0.95 + 0.90 COMPLEXITY COVEREDRATIO - 0.95 + 0.90 CLASS diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java index 98f9c44ec..32008b8de 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandler.java @@ -33,6 +33,8 @@ import com.sap.cds.services.handler.annotations.HandlerOrder; import com.sap.cds.services.handler.annotations.ServiceName; import java.io.InputStream; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -153,9 +155,9 @@ private void verifyStatus(Path path, Attachments attachment) { "In verify status for content id {} and status {}", attachment.getContentId(), currentStatus); - if (StatusCode.UNSCANNED.equals(currentStatus) - || StatusCode.SCANNING.equals(currentStatus) - || currentStatus == null) { + if (!StatusCode.INFECTED.equals(currentStatus) + && (attachment.getScannedAt() == null + || Instant.now().isAfter(attachment.getScannedAt().plus(3, ChronoUnit.DAYS)))) { logger.debug( "Scanning content with ID {} for malware, has current status {}", attachment.getContentId(), diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java index 588236b4f..7da3be565 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/ReadAttachmentsHandlerTest.java @@ -219,6 +219,7 @@ void scannerCalledForUnscannedAttachments() { attachment.setContentId("some ID"); attachment.setContent(mock(InputStream.class)); attachment.setStatus(StatusCode.UNSCANNED); + attachment.setScannedAt(null); cut.processAfter(readEventContext, List.of(attachment)); @@ -233,6 +234,7 @@ void scannerCalledForUnscannedAttachmentsIfNoContentProvided() { attachment.setContentId("some ID"); attachment.setContent(null); attachment.setStatus(StatusCode.UNSCANNED); + attachment.setScannedAt(null); cut.processAfter(readEventContext, List.of(attachment)); diff --git a/samples/bookshop/app/index.html b/samples/bookshop/app/index.html index 70f631507..3371025f0 100644 --- a/samples/bookshop/app/index.html +++ b/samples/bookshop/app/index.html @@ -15,7 +15,7 @@ - -