Skip to content

Commit 7bdafff

Browse files
CeredronCeredron
andauthored
Allow whitelisted resources to bypass malware scan (#1760)
* Allow whitelisted resources to bypass malware scan * Add to infra * Fix typo * Generalized terminology * Typo * Remove unintended change * Update current status to get correct response * Fix error message on failed malware scan * Align boolean inversion of virus scan concept * Deleted irrelevant elements from test --------- Co-authored-by: Ceredron <roar.mjelde@digdir.no>
1 parent 179e336 commit 7bdafff

File tree

14 files changed

+122
-20
lines changed

14 files changed

+122
-20
lines changed

.azure/modules/containerApp/main.bicep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var predefinedKeyvaultSecretEnvVars = [
3333
{ name: 'IdportenSettings__ClientId', secretName: 'idporten-client-id' }
3434
{ name: 'IdportenSettings__ClientSecret', secretName: 'idporten-client-secret' }
3535
{ name: 'StatisticsApiKey', secretName: 'statistics-api-key' }
36+
{ name: 'GeneralSettings__MalwareScanBypassWhiteList', secretName: 'malware-scan-bypass-white-list' }
3637
]
3738

3839
var setByPipelineSecretEnvVars = [
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Altinn.Correspondence.Tests.Common
2+
{
3+
public class TestConstants
4+
{
5+
public static readonly string ResourceWhitelistedForMalwareScanBypass = "correspondence-attachment-test-bypass-malware-scan";
6+
}
7+
}

Test/Altinn.Correspondence.Tests/Factories/AttachmentBuilder.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,11 @@ public AttachmentBuilder WithExpirationInDays(int expirationInDays)
5757
_attachment.ExpirationInDays = expirationInDays;
5858
return this;
5959
}
60+
61+
public AttachmentBuilder WithResourceId(string resourceId)
62+
{
63+
_attachment.ResourceId = resourceId;
64+
return this;
65+
}
6066
}
6167
}

Test/Altinn.Correspondence.Tests/Helpers/AttachmentHelper.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
using Altinn.Correspondence.API.Models;
2+
using Altinn.Correspondence.API.Models.Enums;
3+
using Altinn.Correspondence.Tests.Common;
4+
using Altinn.Correspondence.Tests.Factories;
15
using System.Net;
26
using System.Net.Http.Json;
37
using System.Text;
48
using System.Text.Json;
5-
using Altinn.Correspondence.API.Models;
6-
using Altinn.Correspondence.API.Models.Enums;
7-
using Altinn.Correspondence.Tests.Factories;
89

910
namespace Altinn.Correspondence.Tests.Helpers
1011
{
@@ -36,6 +37,24 @@ public static async Task<Guid> GetInitializedAttachment(HttpClient client, JsonS
3637
Assert.Equal(AttachmentStatusExt.Initialized, attachmentOverview?.Status);
3738
return attachmentId;
3839
}
40+
41+
public static async Task<Guid> GetInitializedAttachmentForResourceWhitelistedForBypassMalwareScan(HttpClient client, JsonSerializerOptions responseSerializerOptions, string? sender = null)
42+
{
43+
var tempData = new AttachmentBuilder().CreateAttachment();
44+
if (sender != null)
45+
{
46+
tempData.WithSender(sender);
47+
}
48+
tempData.WithResourceId(TestConstants.ResourceWhitelistedForMalwareScanBypass);
49+
var attachment = tempData.Build();
50+
var initializeAttachmentResponse = await client.PostAsJsonAsync("correspondence/api/v1/attachment", attachment);
51+
Assert.Equal(HttpStatusCode.OK, initializeAttachmentResponse.StatusCode);
52+
var attachmentId = await initializeAttachmentResponse.Content.ReadFromJsonAsync<Guid>();
53+
var attachmentOverview = await (await client.GetAsync($"correspondence/api/v1/attachment/{attachmentId}")).Content.ReadFromJsonAsync<AttachmentOverviewExt>(responseSerializerOptions);
54+
Assert.Equal(AttachmentStatusExt.Initialized, attachmentOverview?.Status);
55+
return attachmentId;
56+
}
57+
3958
public async static Task<Guid> GetPublishedAttachment(HttpClient client, JsonSerializerOptions responseSerializerOptions)
4059
{
4160
var attachment = new AttachmentBuilder().CreateAttachment().Build();

Test/Altinn.Correspondence.Tests/Helpers/CustomWebApplicationFactory.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using System.Text.Json;
2424
using Altinn.Correspondence.Core.Models.Enums;
2525
using Altinn.Correspondence.Common.Helpers;
26+
using Altinn.Correspondence.Tests.Common;
2627

2728
namespace Altinn.Correspondence.Tests.Helpers;
2829

@@ -43,6 +44,10 @@ protected override void ConfigureWebHost(
4344
builder.UseConfiguration(new ConfigurationBuilder()
4445
.AddJsonFile("appsettings.json")
4546
.AddJsonFile("appsettings.Development.json")
47+
.AddInMemoryCollection(new Dictionary<string, string?>
48+
{
49+
["GeneralSettings:MalwareScanBypassWhiteList"] = TestConstants.ResourceWhitelistedForMalwareScanBypass
50+
})
4651
.Build());
4752

4853
// Overwrite registrations from Program.cs

Test/Altinn.Correspondence.Tests/TestingController/Attachment/AttachmentUploadTests.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,15 +240,55 @@ public async Task UploadAttachmentData_InDevelopmentMode_GetsPublishedAfterSimul
240240
Assert.True(hostEnvironment.IsDevelopment(), "Test requires Development environment for automatic malware scan simulation");
241241
var attachmentId = await AttachmentHelper.GetInitializedAttachment(_senderClient, _responseSerializerOptions);
242242
var content = new ByteArrayContent(Encoding.UTF8.GetBytes("Test content"));
243-
243+
244244
// Act
245245
var uploadResponse = await AttachmentHelper.UploadAttachment(attachmentId, _senderClient, content);
246246
Assert.True(uploadResponse.IsSuccessStatusCode, await uploadResponse.Content.ReadAsStringAsync());
247247
var attachmentOverview = await AttachmentHelper.WaitForAttachmentStatusUpdate(_senderClient, _responseSerializerOptions, attachmentId, AttachmentStatusExt.Published);
248-
248+
249249
// Assert
250250
Assert.NotNull(attachmentOverview);
251251
Assert.Equal(AttachmentStatusExt.Published, attachmentOverview.Status);
252252
}
253+
254+
[Fact]
255+
public async Task UploadAttachmentData_ToWhiteListedResource_InstantlyPublished()
256+
{
257+
// Arrange
258+
var attachmentId = await AttachmentHelper.GetInitializedAttachmentForResourceWhitelistedForBypassMalwareScan(_senderClient, _responseSerializerOptions);
259+
var contentBytes = Encoding.UTF8.GetBytes("Test content for checksum");
260+
var content = new ByteArrayContent(contentBytes);
261+
var expectedChecksum = AttachmentHelper.CalculateChecksum(contentBytes);
262+
263+
// Act
264+
var uploadResponse = await AttachmentHelper.UploadAttachment(attachmentId, _senderClient, content);
265+
Assert.True(uploadResponse.IsSuccessStatusCode, await uploadResponse.Content.ReadAsStringAsync());
266+
var uploadedAttachmentOverview = await uploadResponse.Content.ReadFromJsonAsync<AttachmentOverviewExt>(_responseSerializerOptions);
267+
268+
// Assert
269+
Assert.NotNull(uploadedAttachmentOverview);
270+
Assert.Equal(AttachmentStatusExt.Published, uploadedAttachmentOverview.Status);
271+
Assert.Equal(expectedChecksum, uploadedAttachmentOverview.Checksum);
272+
}
273+
274+
[Fact]
275+
public async Task UploadAttachmentData_ToNonWhiteListedResource_NotInstantlyPublished()
276+
{
277+
// Arrange
278+
var attachmentId = await AttachmentHelper.GetInitializedAttachment(_senderClient, _responseSerializerOptions);
279+
var contentBytes = Encoding.UTF8.GetBytes("Test content for checksum");
280+
var content = new ByteArrayContent(contentBytes);
281+
var expectedChecksum = AttachmentHelper.CalculateChecksum(contentBytes);
282+
283+
// Act
284+
var uploadResponse = await AttachmentHelper.UploadAttachment(attachmentId, _senderClient, content);
285+
Assert.True(uploadResponse.IsSuccessStatusCode, await uploadResponse.Content.ReadAsStringAsync());
286+
var uploadedAttachmentOverview = await uploadResponse.Content.ReadFromJsonAsync<AttachmentOverviewExt>(_responseSerializerOptions);
287+
288+
// Assert
289+
Assert.NotNull(uploadedAttachmentOverview);
290+
Assert.NotEqual(AttachmentStatusExt.Published, uploadedAttachmentOverview.Status);
291+
Assert.Equal(expectedChecksum, uploadedAttachmentOverview.Checksum);
292+
}
253293
}
254294
}

src/Altinn.Correspondence.Application/Helpers/AttachmentHelper.cs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using OneOf;
1313
using Hangfire;
1414
using System.Text.RegularExpressions;
15+
using Microsoft.Extensions.Options;
16+
using Altinn.Correspondence.Core.Options;
1517

1618
namespace Altinn.Correspondence.Application.Helpers
1719
{
@@ -24,6 +26,7 @@ public class AttachmentHelper(
2426
IHostEnvironment hostEnvironment,
2527
IBackgroundJobClient backgroundJobClient,
2628
MalwareScanResultHandler malwareScanResultHandler,
29+
IOptions<GeneralSettings> generalSettings,
2730
ILogger<AttachmentHelper> logger)
2831
{
2932

@@ -37,7 +40,7 @@ public class AttachmentHelper(
3740
RegexOptions.IgnoreCase | RegexOptions.Compiled
3841
);
3942

40-
public async Task<OneOf<UploadAttachmentResponse, Error>> UploadAttachment(Stream file, Guid attachmentId, Guid partyUuid, bool forMigration, CancellationToken cancellationToken)
43+
public async Task<OneOf<UploadAttachmentResponse, Error>> UploadAttachment(Stream file, Guid attachmentId, Guid partyUuid, CancellationToken cancellationToken)
4144
{
4245
logger.LogInformation("Start upload of attachment {attachmentId} for party {partyUuid}", attachmentId, partyUuid);
4346
var attachment = await attachmentRepository.GetAttachmentById(attachmentId, true, cancellationToken);
@@ -49,7 +52,8 @@ public async Task<OneOf<UploadAttachmentResponse, Error>> UploadAttachment(Strea
4952

5053
var currentStatus = await SetAttachmentStatus(attachmentId, AttachmentStatus.UploadProcessing, partyUuid, cancellationToken);
5154
logger.LogInformation("Set attachment status of {attachmentId} to UploadProcessing", attachmentId);
52-
var storageProvider = await GetStorageProvider(attachment, forMigration, cancellationToken);
55+
var bypassMalwareScan = ShouldBypassMalwareScan(attachment);
56+
var storageProvider = await GetStorageProvider(attachment, bypassMalwareScan, cancellationToken);
5357
var uploadResult = await UploadBlob(attachment, file, storageProvider, partyUuid, cancellationToken);
5458
if (uploadResult.TryPickT1(out var uploadError, out var successResult))
5559
{
@@ -69,11 +73,15 @@ public async Task<OneOf<UploadAttachmentResponse, Error>> UploadAttachment(Strea
6973

7074
if (!isValidUpdate)
7175
{
72-
await SetAttachmentStatus(attachmentId, AttachmentStatus.Failed, partyUuid, cancellationToken, AttachmentStatusText.UploadFailed);
76+
currentStatus = await SetAttachmentStatus(attachmentId, AttachmentStatus.Failed, partyUuid, cancellationToken, AttachmentStatusText.UploadFailed);
7377
await storageRepository.PurgeAttachment(attachment.Id, attachment.StorageProvider, cancellationToken);
7478
return AttachmentErrors.UploadFailed;
7579
}
76-
if (hostEnvironment.IsDevelopment())
80+
if (bypassMalwareScan)
81+
{
82+
currentStatus = await SetAttachmentStatus(attachmentId, AttachmentStatus.Published, partyUuid, cancellationToken, "Bypassed malware scan");
83+
}
84+
else if (hostEnvironment.IsDevelopment())
7785
{
7886
logger.LogInformation("Development mode detected. Enqueing simulated malware scan result for attachment {attachmentId}", attachmentId);
7987
backgroundJobClient.Enqueue<AttachmentHelper>(helper => helper.SimulateMalwareScanResult(attachmentId));
@@ -91,10 +99,20 @@ public async Task<OneOf<UploadAttachmentResponse, Error>> UploadAttachment(Strea
9199
}, logger, cancellationToken);
92100
}
93101

94-
public async Task<StorageProviderEntity> GetStorageProvider(AttachmentEntity attachment, bool forMigration, CancellationToken cancellationToken)
102+
private bool ShouldBypassMalwareScan(AttachmentEntity attachment)
103+
{
104+
if (string.IsNullOrWhiteSpace(generalSettings.Value.MalwareScanBypassWhiteList))
105+
{
106+
return false;
107+
}
108+
var whiteList = generalSettings.Value.MalwareScanBypassWhiteList.Split(',').ToList();
109+
return whiteList.Any(whiteListedResource => attachment.ResourceId == whiteListedResource);
110+
}
111+
112+
public async Task<StorageProviderEntity> GetStorageProvider(AttachmentEntity attachment, bool bypassMalwareScan, CancellationToken cancellationToken)
95113
{
96114
ServiceOwnerEntity? serviceOwnerEntity = null;
97-
if (forMigration)
115+
if (bypassMalwareScan)
98116
{
99117
var serviceOwnerShortHand = attachment.ResourceId.Split('-')[0];
100118
serviceOwnerEntity = await serviceOwnerRepository.GetServiceOwnerByOrgCode(serviceOwnerShortHand.ToLower(), cancellationToken);
@@ -114,7 +132,7 @@ public async Task<StorageProviderEntity> GetStorageProvider(AttachmentEntity att
114132
logger.LogError($"Could not find service owner entity for {attachment.ResourceId} in database");
115133
//return AttachmentErrors.ServiceOwnerNotFound; // Future PR will add service owner registry as requirement when we have ensured that existing service owners have been provisioned
116134
}
117-
return serviceOwnerEntity?.GetStorageProvider(forMigration ? false : true);
135+
return serviceOwnerEntity?.GetStorageProvider(bypassMalwareScan: bypassMalwareScan);
118136
}
119137

120138
private async Task<OneOf<(string? locationUrl, string? hash, long size),Error>> UploadBlob(AttachmentEntity attachment, Stream stream, StorageProviderEntity? storageProvider, Guid partyUuid, CancellationToken cancellationToken)

src/Altinn.Correspondence.Application/Helpers/InitializeCorrespondenceHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ public CorrespondenceStatus GetCurrentCorrespondenceStatus(CorrespondenceEntity
447447
OneOf<UploadAttachmentResponse, Error> uploadResponse;
448448
await using (var f = file.OpenReadStream())
449449
{
450-
uploadResponse = await attachmentHelper.UploadAttachment(f, attachment.Id, partyUuid, forMigration: false, cancellationToken);
450+
uploadResponse = await attachmentHelper.UploadAttachment(f, attachment.Id, partyUuid, cancellationToken);
451451
}
452452
var error = uploadResponse.Match(
453453
_ => { return null; },

src/Altinn.Correspondence.Application/MalwareScanResult/MalwareScanResultHandler.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using OneOf;
1111
using System.Security.Claims;
1212
using Altinn.Correspondence.Application.ExpireAttachment;
13+
using System.Text.Json;
1314

1415
namespace Altinn.Correspondence.Application;
1516

@@ -87,7 +88,7 @@ await attachmentStatusRepository.AddAttachmentStatus(new AttachmentStatusEntity(
8788
AttachmentId = attachmentId,
8889
Status = AttachmentStatus.Failed,
8990
StatusChanged = DateTimeOffset.UtcNow,
90-
StatusText = $"Malware scan failed: {data.ScanResultType}",
91+
StatusText = $"Malware scan failed: {data.ScanResultDetails?.ErrorMessage ?? data.ScanResultType}",
9192
PartyUuid = partyUuid
9293
}, cancellationToken);
9394
backgroundJobClient.Enqueue<IEventBus>((eventBus) => eventBus.Publish(
@@ -101,7 +102,7 @@ await attachmentStatusRepository.AddAttachmentStatus(new AttachmentStatusEntity(
101102
attachmentId,
102103
attachment.FileName,
103104
data.ScanResultType,
104-
data.ScanResultDetails);
105+
JsonSerializer.Serialize(data.ScanResultDetails));
105106
backgroundJobClient.Enqueue(() => FailAssociatedCorrespondences(attachmentId, partyUuid, CancellationToken.None));
106107
return Task.CompletedTask;
107108
}, logger, cancellationToken);

src/Altinn.Correspondence.Application/MalwareScanResult/Models/ScanResultData.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Text.Json.Serialization;
1+
using Azure.Core.Serialization;
2+
using System.Text.Json.Serialization;
23

34
namespace Altinn.Correspondence.Application.MalwareScanResult.Models;
45

@@ -9,6 +10,9 @@ public class ScanResultDetails
910

1011
[JsonPropertyName("sha256")]
1112
public string Sha256 { get; set; }
13+
14+
[JsonPropertyName("errorMessage")]
15+
public string? ErrorMessage { get; set; }
1216
}
1317

1418
public class ScanResultData

0 commit comments

Comments
 (0)