Files
tiku-backend.net/Tiku.Infrastructure/Assets/Security/AssetSecurityScanJobHandler.cs
xiong 7adcb6a3d5
Some checks failed
ci / release-gate (push) Has been cancelled
refactor(jobs): modularize background job handlers
2026-08-03 13:28:21 +08:00

119 lines
5.4 KiB
C#

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);
}
}