test fixes and new product advisories work
This commit is contained in:
@@ -5,6 +5,11 @@ namespace StellaOps.AirGap.Importer.Validation;
|
||||
/// </summary>
|
||||
public sealed record BundleValidationResult(bool IsValid, string Reason)
|
||||
{
|
||||
/// <summary>
|
||||
/// Summary of referrer validation results (if referrer validation was performed).
|
||||
/// </summary>
|
||||
public ReferrerValidationSummary? ReferrerSummary { get; init; }
|
||||
|
||||
public static BundleValidationResult Success(string reason = "ok") => new(true, reason);
|
||||
public static BundleValidationResult Failure(string reason) => new(false, reason);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ using StellaOps.AirGap.Importer.Versioning;
|
||||
namespace StellaOps.AirGap.Importer.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Coordinates DSSE, TUF, Merkle, monotonicity, and quarantine behaviors for an offline import.
|
||||
/// Coordinates DSSE, TUF, Merkle, monotonicity, referrer validation, and quarantine behaviors for an offline import.
|
||||
/// </summary>
|
||||
public sealed class ImportValidator
|
||||
{
|
||||
@@ -19,6 +19,7 @@ public sealed class ImportValidator
|
||||
private readonly RootRotationPolicy _rotation;
|
||||
private readonly IVersionMonotonicityChecker _monotonicityChecker;
|
||||
private readonly IQuarantineService _quarantineService;
|
||||
private readonly ReferrerValidator? _referrerValidator;
|
||||
private readonly ILogger<ImportValidator> _logger;
|
||||
|
||||
public ImportValidator(
|
||||
@@ -28,7 +29,8 @@ public sealed class ImportValidator
|
||||
RootRotationPolicy rotation,
|
||||
IVersionMonotonicityChecker monotonicityChecker,
|
||||
IQuarantineService quarantineService,
|
||||
ILogger<ImportValidator> logger)
|
||||
ILogger<ImportValidator> logger,
|
||||
ReferrerValidator? referrerValidator = null)
|
||||
{
|
||||
_dsse = dsse ?? throw new ArgumentNullException(nameof(dsse));
|
||||
_tuf = tuf ?? throw new ArgumentNullException(nameof(tuf));
|
||||
@@ -36,6 +38,7 @@ public sealed class ImportValidator
|
||||
_rotation = rotation ?? throw new ArgumentNullException(nameof(rotation));
|
||||
_monotonicityChecker = monotonicityChecker ?? throw new ArgumentNullException(nameof(monotonicityChecker));
|
||||
_quarantineService = quarantineService ?? throw new ArgumentNullException(nameof(quarantineService));
|
||||
_referrerValidator = referrerValidator;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -152,6 +155,45 @@ public sealed class ImportValidator
|
||||
}
|
||||
verificationLog.Add($"rotation:{rotationResult.Reason}");
|
||||
|
||||
// Referrer validation (if validator is provided and bundle type supports it)
|
||||
ReferrerValidationSummary? referrerSummary = null;
|
||||
if (_referrerValidator is not null && IsBundleTypeWithReferrers(request.BundleType))
|
||||
{
|
||||
referrerSummary = _referrerValidator.Validate(
|
||||
request.ManifestJson,
|
||||
request.PayloadEntries,
|
||||
cancellationToken);
|
||||
|
||||
if (!referrerSummary.IsValid)
|
||||
{
|
||||
var errorDetails = FormatReferrerErrors(referrerSummary);
|
||||
var failed = BundleValidationResult.Failure($"referrer-validation-failed:{errorDetails}");
|
||||
verificationLog.Add(failed.Reason);
|
||||
_logger.LogWarning(
|
||||
"offlinekit.import.validation failed tenant_id={tenant_id} bundle_type={bundle_type} bundle_digest={bundle_digest} reason_code={reason_code} referrer_missing={missing} checksum_mismatch={checksum} size_mismatch={size}",
|
||||
request.TenantId,
|
||||
request.BundleType,
|
||||
request.BundleDigest,
|
||||
"REFERRER_VALIDATION_FAILED",
|
||||
referrerSummary.MissingReferrers,
|
||||
referrerSummary.ChecksumMismatches,
|
||||
referrerSummary.SizeMismatches);
|
||||
await TryQuarantineAsync(request, failed, verificationLog, cancellationToken).ConfigureAwait(false);
|
||||
return failed with { ReferrerSummary = referrerSummary };
|
||||
}
|
||||
|
||||
if (referrerSummary.OrphanedReferrers > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"offlinekit.import.referrer_orphans tenant_id={tenant_id} bundle_type={bundle_type} orphaned_count={orphaned_count}",
|
||||
request.TenantId,
|
||||
request.BundleType,
|
||||
referrerSummary.OrphanedReferrers);
|
||||
}
|
||||
|
||||
verificationLog.Add($"referrers:valid={referrerSummary.ValidReferrers}:total={referrerSummary.TotalReferrers}");
|
||||
}
|
||||
|
||||
BundleVersion incomingVersion;
|
||||
try
|
||||
{
|
||||
@@ -254,7 +296,7 @@ public sealed class ImportValidator
|
||||
request.BundleDigest,
|
||||
request.ManifestVersion,
|
||||
request.ForceActivate);
|
||||
return BundleValidationResult.Success("import-validated");
|
||||
return BundleValidationResult.Success("import-validated") with { ReferrerSummary = referrerSummary };
|
||||
}
|
||||
|
||||
private async Task TryQuarantineAsync(
|
||||
@@ -355,6 +397,35 @@ public sealed class ImportValidator
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsBundleTypeWithReferrers(string bundleType)
|
||||
{
|
||||
// Only mirror bundles and offline kits containing mirror bundles support referrers
|
||||
return bundleType.Equals("mirror-bundle", StringComparison.OrdinalIgnoreCase) ||
|
||||
bundleType.Equals("offline-kit", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string FormatReferrerErrors(ReferrerValidationSummary summary)
|
||||
{
|
||||
var parts = new List<string>(3);
|
||||
|
||||
if (summary.MissingReferrers > 0)
|
||||
{
|
||||
parts.Add($"missing={summary.MissingReferrers}");
|
||||
}
|
||||
|
||||
if (summary.ChecksumMismatches > 0)
|
||||
{
|
||||
parts.Add($"checksum_mismatch={summary.ChecksumMismatches}");
|
||||
}
|
||||
|
||||
if (summary.SizeMismatches > 0)
|
||||
{
|
||||
parts.Add($"size_mismatch={summary.SizeMismatches}");
|
||||
}
|
||||
|
||||
return parts.Count > 0 ? string.Join(",", parts) : "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ImportValidationRequest(
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.AirGap.Importer.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Validates OCI referrer artifacts declared in a mirror bundle manifest.
|
||||
/// </summary>
|
||||
public sealed class ReferrerValidator
|
||||
{
|
||||
private readonly ILogger<ReferrerValidator> _logger;
|
||||
|
||||
public ReferrerValidator(ILogger<ReferrerValidator> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates referrer artifacts in a bundle against manifest declarations.
|
||||
/// </summary>
|
||||
/// <param name="manifestJson">The bundle manifest JSON containing referrers section.</param>
|
||||
/// <param name="bundleEntries">Named streams of bundle entries for content validation.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Referrer validation summary with any issues found.</returns>
|
||||
public ReferrerValidationSummary Validate(
|
||||
string? manifestJson,
|
||||
IReadOnlyList<NamedStream> bundleEntries,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(manifestJson))
|
||||
{
|
||||
return ReferrerValidationSummary.Empty();
|
||||
}
|
||||
|
||||
var referrers = TryParseReferrersSection(manifestJson);
|
||||
if (referrers is null || referrers.Count == 0)
|
||||
{
|
||||
// No referrers declared; check for orphans
|
||||
var orphans = FindOrphanedReferrers(bundleEntries, new HashSet<string>(StringComparer.OrdinalIgnoreCase));
|
||||
return new ReferrerValidationSummary
|
||||
{
|
||||
TotalSubjects = 0,
|
||||
TotalReferrers = 0,
|
||||
ValidReferrers = 0,
|
||||
MissingReferrers = 0,
|
||||
ChecksumMismatches = 0,
|
||||
SizeMismatches = 0,
|
||||
OrphanedReferrers = orphans.Count,
|
||||
Issues = orphans
|
||||
};
|
||||
}
|
||||
|
||||
var issues = new List<ReferrerValidationIssue>();
|
||||
var validCount = 0;
|
||||
var missingCount = 0;
|
||||
var checksumMismatchCount = 0;
|
||||
var sizeMismatchCount = 0;
|
||||
|
||||
// Build lookup of bundle entries by path
|
||||
var entryLookup = bundleEntries
|
||||
.ToDictionary(e => NormalizePath(e.Path), e => e, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Track which paths we've validated (for orphan detection)
|
||||
var validatedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var referrer in referrers)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var normalizedPath = NormalizePath(referrer.Path);
|
||||
validatedPaths.Add(normalizedPath);
|
||||
|
||||
if (!entryLookup.TryGetValue(normalizedPath, out var entry))
|
||||
{
|
||||
missingCount++;
|
||||
issues.Add(new ReferrerValidationIssue
|
||||
{
|
||||
IssueType = ReferrerValidationIssueType.ReferrerMissing,
|
||||
Severity = ReferrerValidationSeverity.Error,
|
||||
SubjectDigest = referrer.SubjectDigest,
|
||||
ReferrerDigest = referrer.Digest,
|
||||
ExpectedPath = referrer.Path,
|
||||
Message = $"Declared referrer artifact not found in bundle: {referrer.Path}"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate checksum
|
||||
var actualChecksum = ComputeStreamChecksum(entry.Stream);
|
||||
if (!string.Equals(actualChecksum, referrer.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
checksumMismatchCount++;
|
||||
issues.Add(new ReferrerValidationIssue
|
||||
{
|
||||
IssueType = ReferrerValidationIssueType.ReferrerChecksumMismatch,
|
||||
Severity = ReferrerValidationSeverity.Error,
|
||||
SubjectDigest = referrer.SubjectDigest,
|
||||
ReferrerDigest = referrer.Digest,
|
||||
ExpectedPath = referrer.Path,
|
||||
ExpectedValue = referrer.Sha256,
|
||||
ActualValue = actualChecksum,
|
||||
Message = $"Referrer artifact checksum mismatch: expected {referrer.Sha256}, got {actualChecksum}"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate size
|
||||
var actualSize = GetStreamLength(entry.Stream);
|
||||
if (referrer.Size > 0 && actualSize != referrer.Size)
|
||||
{
|
||||
sizeMismatchCount++;
|
||||
issues.Add(new ReferrerValidationIssue
|
||||
{
|
||||
IssueType = ReferrerValidationIssueType.ReferrerSizeMismatch,
|
||||
Severity = ReferrerValidationSeverity.Error,
|
||||
SubjectDigest = referrer.SubjectDigest,
|
||||
ReferrerDigest = referrer.Digest,
|
||||
ExpectedPath = referrer.Path,
|
||||
ExpectedValue = referrer.Size.ToString(),
|
||||
ActualValue = actualSize.ToString(),
|
||||
Message = $"Referrer artifact size mismatch: expected {referrer.Size} bytes, got {actualSize} bytes"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
validCount++;
|
||||
}
|
||||
|
||||
// Find orphaned referrer artifacts (files in referrers/ not declared in manifest)
|
||||
var orphanedIssues = FindOrphanedReferrers(bundleEntries, validatedPaths);
|
||||
issues.AddRange(orphanedIssues);
|
||||
|
||||
// Count unique subjects
|
||||
var subjectCount = referrers.Select(r => r.SubjectDigest).Distinct(StringComparer.OrdinalIgnoreCase).Count();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Referrer validation completed: subjects={subjects} total={total} valid={valid} missing={missing} checksum_mismatch={checksum_mismatch} size_mismatch={size_mismatch} orphaned={orphaned}",
|
||||
subjectCount,
|
||||
referrers.Count,
|
||||
validCount,
|
||||
missingCount,
|
||||
checksumMismatchCount,
|
||||
sizeMismatchCount,
|
||||
orphanedIssues.Count);
|
||||
|
||||
return new ReferrerValidationSummary
|
||||
{
|
||||
TotalSubjects = subjectCount,
|
||||
TotalReferrers = referrers.Count,
|
||||
ValidReferrers = validCount,
|
||||
MissingReferrers = missingCount,
|
||||
ChecksumMismatches = checksumMismatchCount,
|
||||
SizeMismatches = sizeMismatchCount,
|
||||
OrphanedReferrers = orphanedIssues.Count,
|
||||
Issues = issues
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the validation summary represents a passing state.
|
||||
/// Missing referrers and checksum/size mismatches are failures.
|
||||
/// Orphaned referrers are warnings only.
|
||||
/// </summary>
|
||||
public static bool IsValid(ReferrerValidationSummary summary)
|
||||
{
|
||||
return summary.MissingReferrers == 0 &&
|
||||
summary.ChecksumMismatches == 0 &&
|
||||
summary.SizeMismatches == 0;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ParsedReferrer>? TryParseReferrersSection(string manifestJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(manifestJson);
|
||||
|
||||
// Look for referrers section (can be top-level or nested)
|
||||
if (!doc.RootElement.TryGetProperty("referrers", out var referrersElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse subjects array
|
||||
if (!referrersElement.TryGetProperty("subjects", out var subjectsElement) ||
|
||||
subjectsElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var referrers = new List<ParsedReferrer>();
|
||||
|
||||
foreach (var subject in subjectsElement.EnumerateArray())
|
||||
{
|
||||
var subjectDigest = GetStringProperty(subject, "subject");
|
||||
if (string.IsNullOrEmpty(subjectDigest))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!subject.TryGetProperty("artifacts", out var artifactsElement) ||
|
||||
artifactsElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var artifact in artifactsElement.EnumerateArray())
|
||||
{
|
||||
var digest = GetStringProperty(artifact, "digest");
|
||||
var path = GetStringProperty(artifact, "path");
|
||||
var sha256 = GetStringProperty(artifact, "sha256");
|
||||
var size = GetLongProperty(artifact, "size");
|
||||
var category = GetStringProperty(artifact, "category");
|
||||
var artifactType = GetStringProperty(artifact, "artifactType");
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
referrers.Add(new ParsedReferrer(
|
||||
SubjectDigest: subjectDigest,
|
||||
Digest: digest ?? string.Empty,
|
||||
Path: path,
|
||||
Sha256: sha256 ?? string.Empty,
|
||||
Size: size,
|
||||
Category: category ?? string.Empty,
|
||||
ArtifactType: artifactType));
|
||||
}
|
||||
}
|
||||
|
||||
return referrers;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ReferrerValidationIssue> FindOrphanedReferrers(
|
||||
IReadOnlyList<NamedStream> bundleEntries,
|
||||
HashSet<string> validatedPaths)
|
||||
{
|
||||
var orphans = new List<ReferrerValidationIssue>();
|
||||
|
||||
foreach (var entry in bundleEntries)
|
||||
{
|
||||
var normalizedPath = NormalizePath(entry.Path);
|
||||
|
||||
// Check if this is a referrer artifact (under referrers/ directory)
|
||||
if (!normalizedPath.StartsWith("referrers/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if already validated
|
||||
if (validatedPaths.Contains(normalizedPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
orphans.Add(new ReferrerValidationIssue
|
||||
{
|
||||
IssueType = ReferrerValidationIssueType.OrphanedReferrer,
|
||||
Severity = ReferrerValidationSeverity.Warning,
|
||||
ExpectedPath = entry.Path,
|
||||
Message = $"Referrer artifact exists but is not declared in manifest: {entry.Path}"
|
||||
});
|
||||
}
|
||||
|
||||
return orphans;
|
||||
}
|
||||
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
return path.Replace('\\', '/').TrimStart('/');
|
||||
}
|
||||
|
||||
private static string ComputeStreamChecksum(Stream stream)
|
||||
{
|
||||
var canSeek = stream.CanSeek;
|
||||
var originalPosition = canSeek ? stream.Position : 0;
|
||||
|
||||
if (canSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
var hash = SHA256.HashData(stream);
|
||||
|
||||
if (canSeek)
|
||||
{
|
||||
stream.Seek(originalPosition, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static long GetStreamLength(Stream stream)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
return stream.Length;
|
||||
}
|
||||
|
||||
// For non-seekable streams, we already computed the hash so position is at end
|
||||
return stream.Position;
|
||||
}
|
||||
|
||||
private static string? GetStringProperty(JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return prop.GetString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long GetLongProperty(JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
return prop.GetInt64();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private sealed record ParsedReferrer(
|
||||
string SubjectDigest,
|
||||
string Digest,
|
||||
string Path,
|
||||
string Sha256,
|
||||
long Size,
|
||||
string Category,
|
||||
string? ArtifactType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary of referrer validation results.
|
||||
/// </summary>
|
||||
public sealed record ReferrerValidationSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of unique subject images with declared referrers.
|
||||
/// </summary>
|
||||
public int TotalSubjects { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of declared referrer artifacts.
|
||||
/// </summary>
|
||||
public int TotalReferrers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of referrers that passed validation.
|
||||
/// </summary>
|
||||
public int ValidReferrers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of declared referrers not found in bundle.
|
||||
/// </summary>
|
||||
public int MissingReferrers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of referrers with checksum mismatches.
|
||||
/// </summary>
|
||||
public int ChecksumMismatches { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of referrers with size mismatches.
|
||||
/// </summary>
|
||||
public int SizeMismatches { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of undeclared referrer artifacts found in bundle.
|
||||
/// </summary>
|
||||
public int OrphanedReferrers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detailed list of validation issues.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ReferrerValidationIssue> Issues { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty summary when no referrers are present.
|
||||
/// </summary>
|
||||
public static ReferrerValidationSummary Empty() => new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the validation passed (no errors, warnings are allowed).
|
||||
/// </summary>
|
||||
public bool IsValid => MissingReferrers == 0 && ChecksumMismatches == 0 && SizeMismatches == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A specific validation issue found during referrer validation.
|
||||
/// </summary>
|
||||
public sealed record ReferrerValidationIssue
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of validation issue.
|
||||
/// </summary>
|
||||
public required ReferrerValidationIssueType IssueType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Severity of the issue.
|
||||
/// </summary>
|
||||
public required ReferrerValidationSeverity Severity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject image digest (if applicable).
|
||||
/// </summary>
|
||||
public string? SubjectDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Referrer artifact digest (if applicable).
|
||||
/// </summary>
|
||||
public string? ReferrerDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected path in the bundle.
|
||||
/// </summary>
|
||||
public string? ExpectedPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected value (for mismatch issues).
|
||||
/// </summary>
|
||||
public string? ExpectedValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual value found (for mismatch issues).
|
||||
/// </summary>
|
||||
public string? ActualValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable description of the issue.
|
||||
/// </summary>
|
||||
public required string Message { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types of referrer validation issues.
|
||||
/// </summary>
|
||||
public enum ReferrerValidationIssueType
|
||||
{
|
||||
/// <summary>
|
||||
/// Declared referrer artifact not found in bundle.
|
||||
/// </summary>
|
||||
ReferrerMissing = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Referrer artifact checksum doesn't match declared value.
|
||||
/// </summary>
|
||||
ReferrerChecksumMismatch = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Referrer artifact size doesn't match declared value.
|
||||
/// </summary>
|
||||
ReferrerSizeMismatch = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Artifact found in referrers/ directory but not declared in manifest.
|
||||
/// </summary>
|
||||
OrphanedReferrer = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Severity levels for referrer validation issues.
|
||||
/// </summary>
|
||||
public enum ReferrerValidationSeverity
|
||||
{
|
||||
/// <summary>
|
||||
/// Warning - does not fail validation.
|
||||
/// </summary>
|
||||
Warning = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Error - fails validation.
|
||||
/// </summary>
|
||||
Error = 2
|
||||
}
|
||||
Reference in New Issue
Block a user