refactor(jobs): modularize background job handlers
Some checks failed
ci / release-gate (push) Has been cancelled
Some checks failed
ci / release-gate (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Storage;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Infrastructure.Jobs;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Assets.Security;
|
||||
|
||||
internal sealed class AssetSecurityScanJobHandler(
|
||||
TikuDbContext dbContext,
|
||||
IAssetSecurityScanner scanner,
|
||||
IObjectStorageService storage) : IBackgroundJobHandler
|
||||
{
|
||||
public string JobType => "asset_security_scan";
|
||||
|
||||
public async Task<BackgroundJobHandlerResult> HandleAsync(
|
||||
BackgroundJobExecutionContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var assetId = BackgroundJobPayload.GetGuid(context.Payload, "assetId") ??
|
||||
throw new InvalidOperationException("asset_security_scan job requires assetId.");
|
||||
var asset = await dbContext.ContentAssets.SingleOrDefaultAsync(
|
||||
item => item.TenantId == context.TenantId && item.Id == assetId,
|
||||
cancellationToken) ?? throw new InvalidOperationException("Asset security scan target was not found.");
|
||||
if (asset.UploadStatus != AssetUploadStatus.Verified ||
|
||||
string.IsNullOrWhiteSpace(asset.Bucket) ||
|
||||
string.IsNullOrWhiteSpace(asset.ObjectKey))
|
||||
throw new InvalidOperationException("Asset must have a verified object location before security scanning.");
|
||||
|
||||
asset.SecurityScanStatus = AssetSecurityScanStatus.Scanning;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await using var content = await storage.OpenReadAsync(
|
||||
new ObjectStorageReadRequest(
|
||||
context.TenantId,
|
||||
asset.StorageProvider switch
|
||||
{
|
||||
AssetStorageProvider.AliyunOss => ObjectStorageProviders.AliyunOss,
|
||||
AssetStorageProvider.LocalDev => ObjectStorageProviders.LocalDev,
|
||||
_ => throw new InvalidOperationException(
|
||||
"Asset storage provider does not support security scanning.")
|
||||
},
|
||||
asset.Bucket,
|
||||
asset.ObjectKey),
|
||||
cancellationToken);
|
||||
var result =
|
||||
await scanner.ScanAsync(content, asset.VerifiedSizeBytes ?? asset.FileSizeBytes, cancellationToken);
|
||||
var infected = result.Verdict == AssetSecurityScanVerdict.Infected;
|
||||
asset.SecurityScanStatus = infected ? AssetSecurityScanStatus.Failed : AssetSecurityScanStatus.Passed;
|
||||
asset.SecurityScannedAt = DateTimeOffset.UtcNow;
|
||||
asset.SecurityScanProvider = result.Provider;
|
||||
asset.SecurityScanSummary = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
verdict = result.Verdict.ToString(),
|
||||
result.Signature,
|
||||
result.BytesScanned
|
||||
});
|
||||
dbContext.ContentAssetSecurityScanEvents.Add(new ContentAssetSecurityScanEvent
|
||||
{
|
||||
TenantId = context.TenantId,
|
||||
AssetId = asset.Id,
|
||||
Provider = result.Provider,
|
||||
ScanStatus = asset.SecurityScanStatus,
|
||||
RiskLevel = infected ? AssetSecurityRiskLevel.Critical : AssetSecurityRiskLevel.None,
|
||||
IssueCodes = infected ? [result.Signature ?? "malware_detected"] : [],
|
||||
Details = asset.SecurityScanSummary
|
||||
});
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new BackgroundJobHandlerResult(JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
assetId = asset.Id,
|
||||
status = asset.SecurityScanStatus.ToString(),
|
||||
result.Signature,
|
||||
result.BytesScanned
|
||||
}));
|
||||
}
|
||||
catch (AssetSecurityScannerException exception)
|
||||
{
|
||||
throw new BackgroundJobHandlerException(exception.Code, exception.Message, exception);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleFailureAsync(
|
||||
BackgroundJobExecutionContext context,
|
||||
Exception exception,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (exception is not BackgroundJobHandlerException handlerException ||
|
||||
handlerException.InnerException is not AssetSecurityScannerException)
|
||||
return;
|
||||
|
||||
var assetId = BackgroundJobPayload.GetGuid(context.Payload, "assetId");
|
||||
if (assetId is null) return;
|
||||
var asset = await dbContext.ContentAssets.SingleOrDefaultAsync(
|
||||
item => item.TenantId == context.TenantId && item.Id == assetId,
|
||||
cancellationToken);
|
||||
if (asset is null) return;
|
||||
|
||||
asset.SecurityScanStatus = AssetSecurityScanStatus.Pending;
|
||||
asset.SecurityScanProvider = "clamav";
|
||||
asset.SecurityScanSummary = JsonSerializer.SerializeToElement(new { errorCode = handlerException.Code });
|
||||
dbContext.ContentAssetSecurityScanEvents.Add(new ContentAssetSecurityScanEvent
|
||||
{
|
||||
TenantId = context.TenantId,
|
||||
AssetId = asset.Id,
|
||||
Provider = "clamav",
|
||||
ScanStatus = AssetSecurityScanStatus.Pending,
|
||||
RiskLevel = AssetSecurityRiskLevel.None,
|
||||
IssueCodes = [handlerException.Code],
|
||||
Details = asset.SecurityScanSummary
|
||||
});
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user