using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; using StellaOps.Scanner.Core.Contracts; namespace StellaOps.Scanner.Core.Utility; public static class ScannerIdentifiers { private static readonly Guid ScanJobNamespace = new("d985aa76-8c2b-4cba-bac0-c98c90674f04"); private static readonly Guid CorrelationNamespace = new("7cde18f5-729e-4ea1-be3d-46fda4c55e38"); public static ScanJobId CreateJobId( string imageReference, string? imageDigest = null, string? tenantId = null, string? salt = null) { ArgumentException.ThrowIfNullOrWhiteSpace(imageReference); var normalizedReference = NormalizeImageReference(imageReference); var normalizedDigest = NormalizeDigest(imageDigest) ?? "none"; var normalizedTenant = string.IsNullOrWhiteSpace(tenantId) ? "global" : tenantId.Trim().ToLowerInvariant(); var normalizedSalt = (salt?.Trim() ?? string.Empty).ToLowerInvariant(); using var sha256 = SHA256.Create(); var payload = $"{normalizedReference}|{normalizedDigest}|{normalizedTenant}|{normalizedSalt}"; var hashed = sha256.ComputeHash(Encoding.UTF8.GetBytes(payload)); return new ScanJobId(CreateGuidFromHash(ScanJobNamespace, hashed)); } public static string CreateCorrelationId(ScanJobId jobId, string? stage = null, string? suffix = null) { var normalizedStage = string.IsNullOrWhiteSpace(stage) ? "scan" : stage.Trim().ToLowerInvariant().Replace(' ', '-'); var normalizedSuffix = string.IsNullOrWhiteSpace(suffix) ? string.Empty : "-" + suffix.Trim().ToLowerInvariant().Replace(' ', '-'); return $"scan-{normalizedStage}-{jobId}{normalizedSuffix}"; } public static string CreateDeterministicHash(params string[] segments) { if (segments is null || segments.Length == 0) { throw new ArgumentException("At least one segment must be provided.", nameof(segments)); } using var sha256 = SHA256.Create(); var joined = string.Join('|', segments.Select(static s => s?.Trim() ?? string.Empty)); var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(joined)); return Convert.ToHexString(hash).ToLowerInvariant(); } public static Guid CreateDeterministicGuid(Guid namespaceId, ReadOnlySpan nameBytes) { Span namespaceBytes = stackalloc byte[16]; namespaceId.TryWriteBytes(namespaceBytes); Span buffer = stackalloc byte[namespaceBytes.Length + nameBytes.Length]; namespaceBytes.CopyTo(buffer); nameBytes.CopyTo(buffer[namespaceBytes.Length..]); Span hash = stackalloc byte[32]; SHA256.TryHashData(buffer, hash, out _); Span guidBytes = stackalloc byte[16]; hash[..16].CopyTo(guidBytes); guidBytes[6] = (byte)((guidBytes[6] & 0x0F) | 0x50); guidBytes[8] = (byte)((guidBytes[8] & 0x3F) | 0x80); return new Guid(guidBytes); } public static string NormalizeImageReference(string reference) { ArgumentException.ThrowIfNullOrWhiteSpace(reference); var trimmed = reference.Trim(); var atIndex = trimmed.IndexOf('@'); if (atIndex > 0) { var prefix = trimmed[..atIndex].ToLowerInvariant(); return $"{prefix}{trimmed[atIndex..]}"; } var colonIndex = trimmed.IndexOf(':'); if (colonIndex > 0) { var name = trimmed[..colonIndex].ToLowerInvariant(); var tag = trimmed[(colonIndex + 1)..]; return $"{name}:{tag}"; } return trimmed.ToLowerInvariant(); } public static string? NormalizeDigest(string? digest) { if (string.IsNullOrWhiteSpace(digest)) { return null; } var trimmed = digest.Trim(); var parts = trimmed.Split(':', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length != 2) { return trimmed.ToLowerInvariant(); } return $"{parts[0].ToLowerInvariant()}:{parts[1].ToLowerInvariant()}"; } public static string CreateDeterministicCorrelation(string audience, ScanJobId jobId, string? component = null) { using var sha256 = SHA256.Create(); var payload = $"{audience.Trim().ToLowerInvariant()}|{jobId}|{component?.Trim().ToLowerInvariant() ?? string.Empty}"; var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(payload)); var guid = CreateGuidFromHash(CorrelationNamespace, hash); return $"corr-{guid.ToString("n", CultureInfo.InvariantCulture)}"; } private static Guid CreateGuidFromHash(Guid namespaceId, ReadOnlySpan hash) { Span guidBytes = stackalloc byte[16]; hash[..16].CopyTo(guidBytes); guidBytes[6] = (byte)((guidBytes[6] & 0x0F) | 0x50); guidBytes[8] = (byte)((guidBytes[8] & 0x3F) | 0x80); return new Guid(guidBytes); } }