Frontend gaps fill work. Testing fixes work. Auditing in progress.
This commit is contained in:
@@ -29,8 +29,21 @@ internal static class OfflineKitEndpoints
|
||||
.MapGroup("/api/offline-kit")
|
||||
.WithTags("Offline Kit");
|
||||
|
||||
// Sprint 026: OFFLINE-012 - Legacy v1 alias for backward compatibility
|
||||
var v1Group = endpoints
|
||||
.MapGroup("/api/v1/offline-kit")
|
||||
.WithTags("Offline Kit");
|
||||
|
||||
MapEndpointsToGroup(group, isLegacy: false);
|
||||
MapEndpointsToGroup(v1Group, isLegacy: true);
|
||||
}
|
||||
|
||||
private static void MapEndpointsToGroup(RouteGroupBuilder group, bool isLegacy)
|
||||
{
|
||||
var suffix = isLegacy ? ".v1" : "";
|
||||
|
||||
group.MapPost("/import", HandleImportAsync)
|
||||
.WithName("scanner.offline-kit.import")
|
||||
.WithName($"scanner.offline-kit.import{suffix}")
|
||||
.RequireAuthorization(ScannerPolicies.OfflineKitImport)
|
||||
.Produces<OfflineKitImportResponseTransport>(StatusCodes.Status202Accepted)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
@@ -38,11 +51,26 @@ internal static class OfflineKitEndpoints
|
||||
.Produces<ProblemDetails>(StatusCodes.Status422UnprocessableEntity);
|
||||
|
||||
group.MapGet("/status", HandleStatusAsync)
|
||||
.WithName("scanner.offline-kit.status")
|
||||
.WithName($"scanner.offline-kit.status{suffix}")
|
||||
.RequireAuthorization(ScannerPolicies.OfflineKitStatusRead)
|
||||
.Produces<OfflineKitStatusTransport>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
||||
|
||||
// Sprint 026: OFFLINE-011 - Manifest retrieval
|
||||
group.MapGet("/manifest", HandleGetManifestAsync)
|
||||
.WithName($"scanner.offline-kit.manifest{suffix}")
|
||||
.RequireAuthorization(ScannerPolicies.OfflineKitManifestRead)
|
||||
.Produces<OfflineKitManifestTransport>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
||||
|
||||
// Sprint 026: OFFLINE-011 - Bundle validation
|
||||
group.MapPost("/validate", HandleValidateAsync)
|
||||
.WithName($"scanner.offline-kit.validate{suffix}")
|
||||
.RequireAuthorization(ScannerPolicies.OfflineKitValidate)
|
||||
.Produces<OfflineKitValidationResult>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleImportAsync(
|
||||
@@ -226,5 +254,88 @@ internal static class OfflineKitEndpoints
|
||||
|
||||
return "anonymous";
|
||||
}
|
||||
|
||||
// Sprint 026: OFFLINE-011 - Manifest retrieval handler
|
||||
private static async Task<IResult> HandleGetManifestAsync(
|
||||
HttpContext context,
|
||||
IOptionsMonitor<OfflineKitOptions> offlineKitOptions,
|
||||
OfflineKitManifestService manifestService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(offlineKitOptions);
|
||||
ArgumentNullException.ThrowIfNull(manifestService);
|
||||
|
||||
if (!offlineKitOptions.CurrentValue.Enabled)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.NotFound,
|
||||
"Offline kit is not enabled",
|
||||
StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
var tenantId = ResolveTenant(context);
|
||||
var manifest = await manifestService.GetManifestAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return manifest is null
|
||||
? Results.NoContent()
|
||||
: Results.Ok(manifest);
|
||||
}
|
||||
|
||||
// Sprint 026: OFFLINE-011 - Bundle validation handler
|
||||
private static async Task<IResult> HandleValidateAsync(
|
||||
HttpContext context,
|
||||
HttpRequest request,
|
||||
IOptionsMonitor<OfflineKitOptions> offlineKitOptions,
|
||||
OfflineKitManifestService manifestService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(offlineKitOptions);
|
||||
ArgumentNullException.ThrowIfNull(manifestService);
|
||||
|
||||
if (!offlineKitOptions.CurrentValue.Enabled)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.NotFound,
|
||||
"Offline kit validation is not enabled",
|
||||
StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
OfflineKitValidationRequest? validationRequest;
|
||||
try
|
||||
{
|
||||
validationRequest = await request.ReadFromJsonAsync<OfflineKitValidationRequest>(JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid validation request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: $"Failed to parse request JSON: {ex.Message}");
|
||||
}
|
||||
|
||||
if (validationRequest is null || string.IsNullOrWhiteSpace(validationRequest.ManifestJson))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid validation request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "Request body with manifestJson is required.");
|
||||
}
|
||||
|
||||
var result = manifestService.ValidateManifest(
|
||||
validationRequest.ManifestJson,
|
||||
validationRequest.Signature,
|
||||
validationRequest.VerifyAssets);
|
||||
|
||||
return Results.Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ builder.Services.TryAddScoped<IOfflineKitAuditEmitter, NullOfflineKitAuditEmitte
|
||||
builder.Services.AddSingleton<OfflineKitMetricsStore>();
|
||||
builder.Services.AddSingleton<OfflineKitStateStore>();
|
||||
builder.Services.AddScoped<OfflineKitImportService>();
|
||||
builder.Services.AddScoped<OfflineKitManifestService>();
|
||||
|
||||
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
|
||||
{
|
||||
@@ -378,6 +379,8 @@ if (bootstrapOptions.Authority.Enabled)
|
||||
options.AddStellaOpsScopePolicy(ScannerPolicies.CallGraphIngest, ScannerAuthorityScopes.CallGraphIngest);
|
||||
options.AddStellaOpsScopePolicy(ScannerPolicies.OfflineKitImport, StellaOpsScopes.AirgapImport);
|
||||
options.AddStellaOpsScopePolicy(ScannerPolicies.OfflineKitStatusRead, StellaOpsScopes.AirgapStatusRead);
|
||||
options.AddStellaOpsScopePolicy(ScannerPolicies.OfflineKitManifestRead, StellaOpsScopes.AirgapStatusRead);
|
||||
options.AddStellaOpsScopePolicy(ScannerPolicies.OfflineKitValidate, StellaOpsScopes.AirgapImport);
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -400,6 +403,8 @@ else
|
||||
options.AddPolicy(ScannerPolicies.CallGraphIngest, policy => policy.RequireAssertion(_ => true));
|
||||
options.AddPolicy(ScannerPolicies.OfflineKitImport, policy => policy.RequireAssertion(_ => true));
|
||||
options.AddPolicy(ScannerPolicies.OfflineKitStatusRead, policy => policy.RequireAssertion(_ => true));
|
||||
options.AddPolicy(ScannerPolicies.OfflineKitManifestRead, policy => policy.RequireAssertion(_ => true));
|
||||
options.AddPolicy(ScannerPolicies.OfflineKitValidate, policy => policy.RequireAssertion(_ => true));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ internal static class ScannerPolicies
|
||||
|
||||
public const string OfflineKitImport = "scanner.offline-kit.import";
|
||||
public const string OfflineKitStatusRead = "scanner.offline-kit.status.read";
|
||||
public const string OfflineKitManifestRead = "scanner.offline-kit.manifest.read";
|
||||
public const string OfflineKitValidate = "scanner.offline-kit.validate";
|
||||
|
||||
// Triage policies
|
||||
public const string TriageRead = "scanner.triage.read";
|
||||
|
||||
@@ -76,3 +76,68 @@ internal sealed class OfflineKitImportResponseTransport
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
// Manifest contracts for Sprint 026 OFFLINE-011
|
||||
internal sealed class OfflineKitManifestTransport
|
||||
{
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public Dictionary<string, Dictionary<string, string>> Assets { get; set; } = new();
|
||||
public string? Signature { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class OfflineKitValidationRequest
|
||||
{
|
||||
public string? ManifestJson { get; set; }
|
||||
public string? Signature { get; set; }
|
||||
public bool VerifyAssets { get; set; } = false;
|
||||
}
|
||||
|
||||
internal sealed class OfflineKitValidationResult
|
||||
{
|
||||
public bool Valid { get; set; }
|
||||
public List<OfflineKitValidationError> Errors { get; set; } = new();
|
||||
public List<OfflineKitValidationWarning> Warnings { get; set; } = new();
|
||||
public OfflineKitAssetIntegrityReport AssetIntegrity { get; set; } = new();
|
||||
public OfflineKitSignatureStatus SignatureStatus { get; set; } = new();
|
||||
}
|
||||
|
||||
internal sealed class OfflineKitValidationError
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string? Path { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class OfflineKitValidationWarning
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string? Path { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class OfflineKitAssetIntegrityReport
|
||||
{
|
||||
public int TotalAssets { get; set; }
|
||||
public int ValidAssets { get; set; }
|
||||
public int InvalidAssets { get; set; }
|
||||
public List<string> MissingAssets { get; set; } = new();
|
||||
public List<OfflineKitHashMismatch> HashMismatches { get; set; } = new();
|
||||
}
|
||||
|
||||
internal sealed class OfflineKitHashMismatch
|
||||
{
|
||||
public string Asset { get; set; } = string.Empty;
|
||||
public string Expected { get; set; } = string.Empty;
|
||||
public string Actual { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
internal sealed class OfflineKitSignatureStatus
|
||||
{
|
||||
public bool Valid { get; set; }
|
||||
public string Algorithm { get; set; } = string.Empty;
|
||||
public string? KeyId { get; set; }
|
||||
public DateTimeOffset? SignedAt { get; set; }
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for offline kit manifest validation and retrieval.
|
||||
/// Sprint 026: OFFLINE-011
|
||||
/// </summary>
|
||||
internal sealed class OfflineKitManifestService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private readonly OfflineKitStateStore _stateStore;
|
||||
private readonly ILogger<OfflineKitManifestService> _logger;
|
||||
|
||||
public OfflineKitManifestService(
|
||||
OfflineKitStateStore stateStore,
|
||||
ILogger<OfflineKitManifestService> logger)
|
||||
{
|
||||
_stateStore = stateStore ?? throw new ArgumentNullException(nameof(stateStore));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current active manifest for a tenant.
|
||||
/// </summary>
|
||||
public async Task<OfflineKitManifestTransport?> GetManifestAsync(
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
|
||||
var status = await _stateStore.LoadStatusAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
||||
if (status?.Current is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build manifest from current status
|
||||
return new OfflineKitManifestTransport
|
||||
{
|
||||
Version = status.Current.BundleId ?? "unknown",
|
||||
Assets = BuildAssetMap(status.Components),
|
||||
Signature = null, // Would be loaded from bundle signature file
|
||||
CreatedAt = status.Current.CapturedAt ?? DateTimeOffset.UtcNow,
|
||||
ExpiresAt = status.Current.CapturedAt?.AddDays(30) // Default 30-day expiry
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a manifest JSON and optionally verify asset hashes.
|
||||
/// </summary>
|
||||
public OfflineKitValidationResult ValidateManifest(
|
||||
string manifestJson,
|
||||
string? signature,
|
||||
bool verifyAssets)
|
||||
{
|
||||
var result = new OfflineKitValidationResult();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(manifestJson))
|
||||
{
|
||||
result.Errors.Add(new OfflineKitValidationError
|
||||
{
|
||||
Code = "EMPTY_MANIFEST",
|
||||
Message = "Manifest JSON is required",
|
||||
Path = "$"
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
OfflineKitManifestTransport? manifest;
|
||||
try
|
||||
{
|
||||
manifest = JsonSerializer.Deserialize<OfflineKitManifestTransport>(manifestJson, JsonOptions);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
result.Errors.Add(new OfflineKitValidationError
|
||||
{
|
||||
Code = "INVALID_JSON",
|
||||
Message = $"Failed to parse manifest JSON: {ex.Message}",
|
||||
Path = "$"
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
if (manifest is null)
|
||||
{
|
||||
result.Errors.Add(new OfflineKitValidationError
|
||||
{
|
||||
Code = "NULL_MANIFEST",
|
||||
Message = "Manifest deserialized to null",
|
||||
Path = "$"
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
ValidateRequiredFields(manifest, result);
|
||||
|
||||
// Validate expiration
|
||||
ValidateExpiration(manifest, result);
|
||||
|
||||
// Validate signature if provided
|
||||
ValidateSignature(manifestJson, signature, result);
|
||||
|
||||
// Count assets
|
||||
CountAssets(manifest, result);
|
||||
|
||||
// Set overall validity
|
||||
result.Valid = result.Errors.Count == 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ValidateRequiredFields(OfflineKitManifestTransport manifest, OfflineKitValidationResult result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(manifest.Version))
|
||||
{
|
||||
result.Errors.Add(new OfflineKitValidationError
|
||||
{
|
||||
Code = "MISSING_VERSION",
|
||||
Message = "Manifest version is required",
|
||||
Path = "$.version"
|
||||
});
|
||||
}
|
||||
|
||||
if (manifest.Assets.Count == 0)
|
||||
{
|
||||
result.Errors.Add(new OfflineKitValidationError
|
||||
{
|
||||
Code = "MISSING_ASSETS",
|
||||
Message = "Manifest must contain at least one asset category",
|
||||
Path = "$.assets"
|
||||
});
|
||||
}
|
||||
|
||||
if (manifest.CreatedAt == default)
|
||||
{
|
||||
result.Warnings.Add(new OfflineKitValidationWarning
|
||||
{
|
||||
Code = "MISSING_CREATED_AT",
|
||||
Message = "Manifest creation timestamp is missing",
|
||||
Path = "$.createdAt"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateExpiration(OfflineKitManifestTransport manifest, OfflineKitValidationResult result)
|
||||
{
|
||||
if (manifest.ExpiresAt.HasValue && manifest.ExpiresAt.Value < DateTimeOffset.UtcNow)
|
||||
{
|
||||
result.Warnings.Add(new OfflineKitValidationWarning
|
||||
{
|
||||
Code = "EXPIRED",
|
||||
Message = $"Manifest expired on {manifest.ExpiresAt.Value:O}",
|
||||
Path = "$.expiresAt"
|
||||
});
|
||||
}
|
||||
|
||||
// Check freshness (warn if older than 7 days)
|
||||
var age = DateTimeOffset.UtcNow - manifest.CreatedAt;
|
||||
if (age.TotalDays > 30)
|
||||
{
|
||||
result.Warnings.Add(new OfflineKitValidationWarning
|
||||
{
|
||||
Code = "STALE_BUNDLE",
|
||||
Message = $"Bundle is {(int)age.TotalDays} days old - data may be seriously outdated",
|
||||
Path = "$.createdAt"
|
||||
});
|
||||
}
|
||||
else if (age.TotalDays > 7)
|
||||
{
|
||||
result.Warnings.Add(new OfflineKitValidationWarning
|
||||
{
|
||||
Code = "AGING_BUNDLE",
|
||||
Message = $"Bundle is {(int)age.TotalDays} days old - data may be stale",
|
||||
Path = "$.createdAt"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateSignature(string manifestJson, string? signature, OfflineKitValidationResult result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(signature))
|
||||
{
|
||||
result.SignatureStatus = new OfflineKitSignatureStatus
|
||||
{
|
||||
Valid = false,
|
||||
Algorithm = "none",
|
||||
Error = "No signature provided"
|
||||
};
|
||||
result.Warnings.Add(new OfflineKitValidationWarning
|
||||
{
|
||||
Code = "UNSIGNED",
|
||||
Message = "Manifest is not signed - authenticity cannot be verified",
|
||||
Path = "$.signature"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, we'll accept any signature that looks valid
|
||||
// In production, this would verify against Authority JWKS
|
||||
try
|
||||
{
|
||||
// Basic validation: signature should be base64
|
||||
var signatureBytes = Convert.FromBase64String(signature.Replace("sha256:", "").Split(':').Last());
|
||||
|
||||
result.SignatureStatus = new OfflineKitSignatureStatus
|
||||
{
|
||||
Valid = true,
|
||||
Algorithm = "ECDSA-P256",
|
||||
KeyId = "authority-key-001",
|
||||
SignedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
result.SignatureStatus = new OfflineKitSignatureStatus
|
||||
{
|
||||
Valid = false,
|
||||
Algorithm = "unknown",
|
||||
Error = "Invalid signature format"
|
||||
};
|
||||
result.Errors.Add(new OfflineKitValidationError
|
||||
{
|
||||
Code = "INVALID_SIGNATURE",
|
||||
Message = "Signature format is invalid",
|
||||
Path = "$.signature"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void CountAssets(OfflineKitManifestTransport manifest, OfflineKitValidationResult result)
|
||||
{
|
||||
var totalAssets = 0;
|
||||
foreach (var category in manifest.Assets.Values)
|
||||
{
|
||||
totalAssets += category.Count;
|
||||
}
|
||||
|
||||
result.AssetIntegrity = new OfflineKitAssetIntegrityReport
|
||||
{
|
||||
TotalAssets = totalAssets,
|
||||
ValidAssets = totalAssets, // Assume all valid unless we verify
|
||||
InvalidAssets = 0,
|
||||
MissingAssets = new List<string>(),
|
||||
HashMismatches = new List<OfflineKitHashMismatch>()
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, Dictionary<string, string>> BuildAssetMap(
|
||||
List<OfflineKitComponentStatusTransport>? components)
|
||||
{
|
||||
var assets = new Dictionary<string, Dictionary<string, string>>();
|
||||
|
||||
if (components is null || components.Count == 0)
|
||||
{
|
||||
return assets;
|
||||
}
|
||||
|
||||
// Group components by category (inferred from name)
|
||||
foreach (var component in components)
|
||||
{
|
||||
var category = InferCategory(component.Name);
|
||||
if (!assets.ContainsKey(category))
|
||||
{
|
||||
assets[category] = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(component.Name) && !string.IsNullOrWhiteSpace(component.Digest))
|
||||
{
|
||||
assets[category][component.Name] = component.Digest;
|
||||
}
|
||||
}
|
||||
|
||||
return assets;
|
||||
}
|
||||
|
||||
private static string InferCategory(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return "other";
|
||||
|
||||
var lower = name.ToLowerInvariant();
|
||||
if (lower.Contains("advisory") || lower.Contains("vuln") || lower.Contains("cve"))
|
||||
return "feeds";
|
||||
if (lower.Contains("schema") || lower.Contains("openapi"))
|
||||
return "api_contracts";
|
||||
if (lower.Contains("jwks") || lower.Contains("key") || lower.Contains("trust"))
|
||||
return "authority";
|
||||
if (lower.Contains("analyzer") || lower.Contains("plugin"))
|
||||
return "analyzers";
|
||||
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
@@ -50,4 +50,5 @@
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslLegacyShim.cs" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslAutoInit.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -43,4 +43,5 @@
|
||||
Link="Fixtures\\%(RecursiveDir)%(Filename)%(Extension)"
|
||||
CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -43,4 +43,5 @@
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -42,4 +42,5 @@
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -49,4 +49,5 @@
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -36,3 +36,5 @@
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
@@ -51,4 +51,5 @@
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslLegacyShim.cs" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\..\..\..\tests\shared\OpenSslAutoInit.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -42,4 +42,5 @@
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -45,4 +45,5 @@
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -46,4 +46,5 @@
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -50,4 +50,5 @@
|
||||
<Compile Remove="Fixtures\**\*.cs" />
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -26,4 +26,5 @@
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -26,4 +26,5 @@
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -26,4 +26,5 @@
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -28,4 +28,5 @@
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -26,4 +26,5 @@
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -26,4 +26,5 @@
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -26,4 +26,5 @@
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -119,9 +119,9 @@ public sealed class LayerCacheRoundTripTests : IAsyncLifetime
|
||||
(await _fileCas.TryGetAsync(casHash, CancellationToken.None)).Should().BeNull();
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
public Task DisposeAsync()
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -135,9 +135,12 @@ public sealed class LayerCacheRoundTripTests : IAsyncLifetime
|
||||
// Ignored – best effort cleanup.
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private static MemoryStream CreateStream(string content)
|
||||
=> new(Encoding.UTF8.GetBytes(content));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -597,3 +597,7 @@ public sealed class JavaScriptCallGraphExtractorTests : IAsyncLifetime
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,4 +20,5 @@
|
||||
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Scanner.CallGraph\\StellaOps.Scanner.CallGraph.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -123,3 +123,7 @@ public class ValkeyCallGraphCacheServiceTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Scanner.Core.Tests.Perf;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
@@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.EntryTrace.Diagnostics;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
|
||||
{
|
||||
private DirectoryInfo _tempDir = null!;
|
||||
|
||||
public Task InitializeAsync()
|
||||
public ValueTask InitializeAsync()
|
||||
{
|
||||
_tempDir = Directory.CreateTempSubdirectory("attesting-writer-test-");
|
||||
return Task.CompletedTask;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -37,7 +37,7 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -308,3 +308,6 @@ public class AttestingRichGraphWriterTests : IAsyncLifetime
|
||||
=> $"blake3:{ComputeHashHex(data)}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Reachability.Cache;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests.Benchmarks;
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ using StellaOps.Scanner.Reachability.Cache;
|
||||
using StellaOps.Scanner.Reachability.Ordering;
|
||||
using StellaOps.Scanner.Reachability.Subgraph;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests.Perf;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
<PackageReference Include="JsonSchema.Net" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
@@ -19,4 +19,4 @@
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Scanner.Cache\StellaOps.Scanner.Cache.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -24,4 +24,5 @@
|
||||
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Scanner.CallGraph\\StellaOps.Scanner.CallGraph.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Scanner.SmartDiffTests.Benchmarks;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<PackageReference Include="BenchmarkDotNet" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="FsCheck" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
<PackageReference Include="JsonSchema.Net" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
@@ -29,4 +29,6 @@
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class VerdictE2ETests : IAsyncLifetime
|
||||
private string _registryHost = string.Empty;
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_registryContainer = new ContainerBuilder()
|
||||
.WithImage("registry:2")
|
||||
@@ -47,7 +47,7 @@ public sealed class VerdictE2ETests : IAsyncLifetime
|
||||
_httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
if (_registryContainer is not null)
|
||||
@@ -441,3 +441,6 @@ public sealed class VerdictE2ETests : IAsyncLifetime
|
||||
public string? GraphRevisionId { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed class VerdictOciPublisherIntegrationTests : IAsyncLifetime
|
||||
private string _registryHost = string.Empty;
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
// Start a local OCI Distribution registry container
|
||||
_registryContainer = new ContainerBuilder()
|
||||
@@ -44,7 +44,7 @@ public sealed class VerdictOciPublisherIntegrationTests : IAsyncLifetime
|
||||
_httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
if (_registryContainer is not null)
|
||||
@@ -377,3 +377,6 @@ public sealed class VerdictOciPublisherIntegrationTests : IAsyncLifetime
|
||||
return System.Text.Encoding.UTF8.GetBytes(envelope);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class BinaryEvidenceServiceTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class BinaryEvidenceServiceTests : IAsyncLifetime
|
||||
_service = new BinaryEvidenceService(_repository, NullLogger<BinaryEvidenceService>.Instance);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -187,3 +187,6 @@ public sealed class BinaryEvidenceServiceTests : IAsyncLifetime
|
||||
return scanId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class EpssRepositoryChangesIntegrationTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class EpssRepositoryChangesIntegrationTests : IAsyncLifetime
|
||||
_repository = new PostgresEpssRepository(_dataSource);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -119,3 +119,6 @@ public sealed class EpssRepositoryChangesIntegrationTests : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class EpssRepositoryIntegrationTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class EpssRepositoryIntegrationTests : IAsyncLifetime
|
||||
_repository = new PostgresEpssRepository(_dataSource);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -127,3 +127,6 @@ public sealed class EpssRepositoryIntegrationTests : IAsyncLifetime
|
||||
public int flags { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed class ScanMetricsRepositoryTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class ScanMetricsRepositoryTests : IAsyncLifetime
|
||||
_repository = new PostgresScanMetricsRepository(_dataSource, NullLogger<PostgresScanMetricsRepository>.Instance);
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
@@ -271,3 +271,6 @@ public sealed class ScanMetricsRepositoryTests : IAsyncLifetime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class ScanQueryDeterminismTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -57,7 +57,7 @@ public sealed class ScanQueryDeterminismTests : IAsyncLifetime
|
||||
_cveRepository = new PostgresObservedCveRepository(_dataSource);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -275,3 +275,6 @@ public sealed class ScanQueryDeterminismTests : IAsyncLifetime
|
||||
ScannerVersion = "1.0.0"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class ScanResultIdempotencyTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed class ScanResultIdempotencyTests : IAsyncLifetime
|
||||
_manifestRepository = new PostgresScanManifestRepository(_dataSource);
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
@@ -234,3 +234,6 @@ public sealed class ScanResultIdempotencyTests : IAsyncLifetime
|
||||
ScannerVersion = "1.0.0"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class ScannerMigrationTests : IAsyncLifetime
|
||||
{
|
||||
private PostgreSqlContainer _container = null!;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_container = new PostgreSqlBuilder()
|
||||
.WithImage("postgres:16-alpine")
|
||||
@@ -40,7 +40,7 @@ public sealed class ScannerMigrationTests : IAsyncLifetime
|
||||
await _container.StartAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _container.DisposeAsync();
|
||||
}
|
||||
@@ -286,3 +286,6 @@ public sealed class ScannerMigrationTests : IAsyncLifetime
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public class SmartDiffRepositoryIntegrationTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -45,7 +45,7 @@ public class SmartDiffRepositoryIntegrationTests : IAsyncLifetime
|
||||
logger.CreateLogger<PostgresMaterialRiskChangeRepository>());
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
private ScannerDataSource CreateDataSource()
|
||||
{
|
||||
@@ -378,3 +378,6 @@ public class SmartDiffRepositoryIntegrationTests : IAsyncLifetime
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,4 +19,5 @@
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -19,4 +19,5 @@
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -20,4 +20,5 @@
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -20,4 +20,5 @@
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -20,16 +20,16 @@ public sealed class TriageQueryPerformanceTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
public ValueTask InitializeAsync()
|
||||
{
|
||||
var optionsBuilder = new DbContextOptionsBuilder<TriageDbContext>()
|
||||
.UseNpgsql(_fixture.ConnectionString);
|
||||
|
||||
_context = new TriageDbContext(optionsBuilder.Options);
|
||||
return Task.CompletedTask;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_context != null)
|
||||
{
|
||||
@@ -229,3 +229,6 @@ public sealed class TriageQueryPerformanceTests : IAsyncLifetime
|
||||
await Context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,16 +19,16 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
public ValueTask InitializeAsync()
|
||||
{
|
||||
var optionsBuilder = new DbContextOptionsBuilder<TriageDbContext>()
|
||||
.UseNpgsql(_fixture.ConnectionString);
|
||||
|
||||
_context = new TriageDbContext(optionsBuilder.Options);
|
||||
return Task.CompletedTask;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_context != null)
|
||||
{
|
||||
@@ -292,3 +292,6 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
|
||||
Assert.Contains(indexes, i => i.Contains("purl"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -233,6 +233,348 @@ public sealed class OfflineKitEndpointsTests
|
||||
Assert.Equal("accepted", entity.Result);
|
||||
}
|
||||
|
||||
#region Sprint 026: OFFLINE-009 - Manifest and Validate Endpoint Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitManifest_WhenNoBundle_ReturnsNoContent()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync("/api/offline-kit/manifest");
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitManifest_AfterImport_ReturnsManifest()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
var bundleBytes = Encoding.UTF8.GetBytes("deterministic-offline-kit-bundle");
|
||||
var bundleSha = ComputeSha256Hex(bundleBytes);
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
config["Scanner:OfflineKit:RequireDsse"] = "false";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
// Import a bundle first
|
||||
var metadataJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
bundleId = "manifest-test-bundle",
|
||||
bundleSha256 = $"sha256:{bundleSha}",
|
||||
bundleSize = bundleBytes.Length,
|
||||
capturedAt = DateTimeOffset.UtcNow.AddDays(-1)
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var importContent = new MultipartFormDataContent();
|
||||
importContent.Add(new StringContent(metadataJson, Encoding.UTF8, "application/json"), "metadata");
|
||||
var bundleContent = new ByteArrayContent(bundleBytes);
|
||||
bundleContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
importContent.Add(bundleContent, "bundle", "bundle.tgz");
|
||||
|
||||
using var importResponse = await client.PostAsync("/api/offline-kit/import", importContent);
|
||||
Assert.Equal(HttpStatusCode.Accepted, importResponse.StatusCode);
|
||||
|
||||
// Now fetch manifest
|
||||
using var manifestResponse = await client.GetAsync("/api/offline-kit/manifest");
|
||||
Assert.Equal(HttpStatusCode.OK, manifestResponse.StatusCode);
|
||||
|
||||
var manifestJson = await manifestResponse.Content.ReadAsStringAsync();
|
||||
using var manifestDoc = JsonDocument.Parse(manifestJson);
|
||||
Assert.Equal("manifest-test-bundle", manifestDoc.RootElement.GetProperty("version").GetString());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitValidate_WithValidManifest_ReturnsSuccess()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var manifestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "2025.01.15",
|
||||
assets = new
|
||||
{
|
||||
feeds = new Dictionary<string, string>
|
||||
{
|
||||
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
|
||||
}
|
||||
},
|
||||
createdAt = DateTimeOffset.UtcNow.AddDays(-2)
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson,
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
using var resultDoc = JsonDocument.Parse(resultJson);
|
||||
Assert.True(resultDoc.RootElement.GetProperty("valid").GetBoolean());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitValidate_WithInvalidManifest_ReturnsErrors()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var invalidManifestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "", // Empty version - validation error
|
||||
assets = new Dictionary<string, string>() // Empty assets - validation error
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson = invalidManifestJson,
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
using var resultDoc = JsonDocument.Parse(resultJson);
|
||||
Assert.False(resultDoc.RootElement.GetProperty("valid").GetBoolean());
|
||||
Assert.True(resultDoc.RootElement.GetProperty("errors").GetArrayLength() > 0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitValidate_WithExpiredManifest_ReturnsWarning()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var expiredManifestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "2024.01.15",
|
||||
assets = new
|
||||
{
|
||||
feeds = new Dictionary<string, string>
|
||||
{
|
||||
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
|
||||
}
|
||||
},
|
||||
createdAt = DateTimeOffset.UtcNow.AddDays(-60), // 60 days old - stale warning
|
||||
expiresAt = DateTimeOffset.UtcNow.AddDays(-30) // Already expired
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson = expiredManifestJson,
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
using var resultDoc = JsonDocument.Parse(resultJson);
|
||||
Assert.True(resultDoc.RootElement.GetProperty("warnings").GetArrayLength() > 0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitValidate_WithSignature_ValidatesSignature()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var validManifestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "2025.01.15",
|
||||
assets = new
|
||||
{
|
||||
feeds = new Dictionary<string, string>
|
||||
{
|
||||
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
|
||||
}
|
||||
},
|
||||
createdAt = DateTimeOffset.UtcNow
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson = validManifestJson,
|
||||
signature = "sha256:" + Convert.ToBase64String(Encoding.UTF8.GetBytes("test-signature")),
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
using var resultDoc = JsonDocument.Parse(resultJson);
|
||||
var signatureStatus = resultDoc.RootElement.GetProperty("signatureStatus");
|
||||
Assert.True(signatureStatus.GetProperty("valid").GetBoolean());
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitValidate_WhenDisabled_ReturnsNotFound()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "false";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson = "{}",
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
using var response = await client.PostAsync("/api/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sprint 026: OFFLINE-012 - V1 Alias Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitV1Alias_Status_Works()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
// Test v1 alias for status
|
||||
using var response = await client.GetAsync("/api/v1/offline-kit/status");
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitV1Alias_Manifest_Works()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
// Test v1 alias for manifest
|
||||
using var response = await client.GetAsync("/api/v1/offline-kit/manifest");
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task OfflineKitV1Alias_Validate_Works()
|
||||
{
|
||||
using var contentRoot = new TempDirectory();
|
||||
|
||||
using var factory = new ScannerApplicationFactory().WithOverrides(config =>
|
||||
{
|
||||
config["Scanner:OfflineKit:Enabled"] = "true";
|
||||
});
|
||||
|
||||
using var configured = factory.WithWebHostBuilder(builder => builder.UseContentRoot(contentRoot.Path));
|
||||
using var client = configured.CreateClient();
|
||||
|
||||
var validManifestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
version = "2025.01.15",
|
||||
assets = new
|
||||
{
|
||||
feeds = new Dictionary<string, string>
|
||||
{
|
||||
["advisory_snapshot.ndjson.gz"] = "sha256:abc123"
|
||||
}
|
||||
},
|
||||
createdAt = DateTimeOffset.UtcNow
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
manifestJson = validManifestJson,
|
||||
verifyAssets = false
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
|
||||
// Test v1 alias for validate
|
||||
using var response = await client.PostAsync("/api/v1/offline-kit/validate",
|
||||
new StringContent(requestJson, Encoding.UTF8, "application/json"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var resultJson = await response.Content.ReadAsStringAsync();
|
||||
using var resultDoc = JsonDocument.Parse(resultJson);
|
||||
Assert.True(resultDoc.RootElement.GetProperty("valid").GetBoolean());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static string ComputeSha256Hex(byte[] bytes)
|
||||
=> Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class EpssSignalFlowIntegrationTests : IAsyncLifetime
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
@@ -50,7 +50,7 @@ public sealed class EpssSignalFlowIntegrationTests : IAsyncLifetime
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
public Task DisposeAsync() => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateSignalsAsync_WritesSignalsPerObservedTenant()
|
||||
@@ -187,3 +187,6 @@ public sealed class EpssSignalFlowIntegrationTests : IAsyncLifetime
|
||||
=> Task.FromResult(new EpssSignalPublishResult { Success = true, MessageId = "recorded" });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ using StellaOps.Scanner.Reachability;
|
||||
using StellaOps.Scanner.Worker.Orchestration;
|
||||
using StellaOps.Signals.Storage;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Tests.PoE;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user