Skip to content

Commit e4444e1

Browse files
authored
Security hardening: credential scoping, S3 encryption, scanner logging (#788)
1 parent fa0e74c commit e4444e1

4 files changed

Lines changed: 55 additions & 25 deletions

File tree

.github/workflows/pipeline.yml

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,6 @@ name: Reusable Workflow
22

33
env:
44
MAVEN_VERSION: '3.9.12'
5-
# Cloud storage environment variables (available to all jobs that need them)
6-
## AWS
7-
AWS_S3_HOST: ${{ secrets.AWS_S3_HOST }}
8-
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
9-
AWS_S3_REGION: ${{ secrets.AWS_S3_REGION }}
10-
AWS_S3_ACCESS_KEY_ID: ${{ secrets.AWS_S3_ACCESS_KEY_ID }}
11-
AWS_S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_S3_SECRET_ACCESS_KEY }}
12-
## Azure
13-
AZURE_CONTAINER_URI: ${{ secrets.AZURE_CONTAINER_URI }}
14-
AZURE_SAS_TOKEN: ${{ secrets.AZURE_SAS_TOKEN }}
15-
## GCP
16-
GS_BASE_64_ENCODED_PRIVATE_KEY_DATA: ${{ secrets.GS_BASE_64_ENCODED_PRIVATE_KEY_DATA }}
17-
GS_BUCKET: ${{ secrets.GS_BUCKET }}
18-
GS_PROJECT_ID: ${{ secrets.GS_PROJECT_ID }}
195

206
on:
217
workflow_call:
@@ -65,6 +51,20 @@ jobs:
6551
runs-on: ubuntu-latest
6652
timeout-minutes: 30
6753
needs: build
54+
env:
55+
## AWS
56+
AWS_S3_HOST: ${{ secrets.AWS_S3_HOST }}
57+
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
58+
AWS_S3_REGION: ${{ secrets.AWS_S3_REGION }}
59+
AWS_S3_ACCESS_KEY_ID: ${{ secrets.AWS_S3_ACCESS_KEY_ID }}
60+
AWS_S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_S3_SECRET_ACCESS_KEY }}
61+
## Azure
62+
AZURE_CONTAINER_URI: ${{ secrets.AZURE_CONTAINER_URI }}
63+
AZURE_SAS_TOKEN: ${{ secrets.AZURE_SAS_TOKEN }}
64+
## GCP
65+
GS_BASE_64_ENCODED_PRIVATE_KEY_DATA: ${{ secrets.GS_BASE_64_ENCODED_PRIVATE_KEY_DATA }}
66+
GS_BUCKET: ${{ secrets.GS_BUCKET }}
67+
GS_PROJECT_ID: ${{ secrets.GS_PROJECT_ID }}
6868
strategy:
6969
fail-fast: false
7070
matrix:

cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/malware/DefaultAttachmentMalwareScanner.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private MalwareScanResultStatus findAndScanAttachments(
9494
return selectionResults.stream()
9595
.filter(result -> validateAndFilter(result, contentId))
9696
.findFirst()
97-
.map(result -> scanDocument(result.result().single(Attachments.class)))
97+
.map(result -> scanDocument(result.result().single(Attachments.class), result.entity))
9898
.orElse(null);
9999
}
100100

@@ -158,7 +158,7 @@ private Result readData(String contentId, CdsEntity entity) {
158158
return result;
159159
}
160160

161-
private MalwareScanResultStatus scanDocument(Attachments attachment) {
161+
private MalwareScanResultStatus scanDocument(Attachments attachment, CdsEntity attachmentEntity) {
162162
if (malwareScanClient != null) {
163163
try {
164164
InputStream content =
@@ -168,7 +168,11 @@ private MalwareScanResultStatus scanDocument(Attachments attachment) {
168168
logger.debug("Start scanning attachment {}.", attachment.getContentId());
169169
return malwareScanClient.scanContent(content);
170170
} catch (RuntimeException e) {
171-
logger.error("Error while scanning attachment {}.", attachment.getContentId(), e);
171+
logger.error(
172+
"Error while scanning attachment {} in entity {}.",
173+
attachment.getContentId(),
174+
attachmentEntity.getQualifiedName(),
175+
e);
172176
return MalwareScanResultStatus.FAILED;
173177
}
174178
}

storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
3333
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
3434
import software.amazon.awssdk.services.s3.model.S3Error;
35+
import software.amazon.awssdk.services.s3.model.ServerSideEncryption;
3536

3637
public class AWSClient implements OSClient {
3738
private final S3Client s3Client;
@@ -89,6 +90,9 @@ public Future<Void> uploadContent(
8990
.bucket(this.bucketName)
9091
.key(completeFileName)
9192
.contentType(contentType)
93+
// Azure and Google Cloud Storage encrypt at rest by default; S3 requires explicit
94+
// opt-in
95+
.serverSideEncryption(ServerSideEncryption.AES256)
9296
.build();
9397

9498
CompletableFuture<PutObjectResponse> putFuture = this.s3AsyncClient.putObject(putRequest, body);

storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
4343
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
4444
import software.amazon.awssdk.services.s3.model.S3Object;
45+
import software.amazon.awssdk.services.s3.model.ServerSideEncryption;
4546

4647
class AWSClientTest {
4748
ExecutorService executor = Executors.newCachedThreadPool();
@@ -73,19 +74,29 @@ void testReadContent() throws Exception {
7374
void testUploadContent() throws Exception {
7475
S3AsyncClient mockAsyncClient = mock(S3AsyncClient.class);
7576
AWSClient awsClient = new AWSClient(mock(S3Client.class), mockAsyncClient, "bucket", executor);
77+
configureSuccessfulPut(mockAsyncClient);
7678

77-
PutObjectResponse mockPutRes = mock(PutObjectResponse.class);
78-
SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class);
79-
when(mockHttpRes.isSuccessful()).thenReturn(true);
80-
when(mockPutRes.sdkHttpResponse()).thenReturn(mockHttpRes);
81-
CompletableFuture<PutObjectResponse> successFuture =
82-
CompletableFuture.completedFuture(mockPutRes);
83-
when(mockAsyncClient.putObject(any(PutObjectRequest.class), any(AsyncRequestBody.class)))
84-
.thenReturn(successFuture);
79+
awsClient
80+
.uploadContent(new ByteArrayInputStream("test".getBytes()), "test.txt", "text/plain")
81+
.get();
82+
}
83+
84+
@Test
85+
void testUploadContentSetsServerSideEncryption() throws Exception {
86+
S3AsyncClient mockAsyncClient = mock(S3AsyncClient.class);
87+
AWSClient awsClient = new AWSClient(mock(S3Client.class), mockAsyncClient, "bucket", executor);
88+
configureSuccessfulPut(mockAsyncClient);
8589

8690
awsClient
8791
.uploadContent(new ByteArrayInputStream("test".getBytes()), "test.txt", "text/plain")
8892
.get();
93+
94+
verify(mockAsyncClient)
95+
.putObject(
96+
argThat(
97+
(PutObjectRequest req) ->
98+
req.serverSideEncryption() == ServerSideEncryption.AES256),
99+
any(AsyncRequestBody.class));
89100
}
90101

91102
@Test
@@ -287,6 +298,17 @@ void testDeleteContentByPrefixWithPagination() throws Exception {
287298
verify(mockS3Client, times(2)).deleteObjects(any(DeleteObjectsRequest.class));
288299
}
289300

301+
private void configureSuccessfulPut(S3AsyncClient mockAsyncClient) {
302+
PutObjectResponse mockPutRes = mock(PutObjectResponse.class);
303+
SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class);
304+
when(mockHttpRes.isSuccessful()).thenReturn(true);
305+
when(mockPutRes.sdkHttpResponse()).thenReturn(mockHttpRes);
306+
CompletableFuture<PutObjectResponse> successFuture =
307+
CompletableFuture.completedFuture(mockPutRes);
308+
when(mockAsyncClient.putObject(any(PutObjectRequest.class), any(AsyncRequestBody.class)))
309+
.thenReturn(successFuture);
310+
}
311+
290312
private ServiceBinding getDummyBinding() {
291313
ServiceBinding binding = mock(ServiceBinding.class);
292314
HashMap<String, Object> creds = new HashMap<>();

0 commit comments

Comments
 (0)