Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			Build Test Deploy / build-test (push) Has been cancelled
				
			Build Test Deploy / authority-container (push) Has been cancelled
				
			Build Test Deploy / docs (push) Has been cancelled
				
			Build Test Deploy / deploy (push) Has been cancelled
				
			
		
			
				
	
	
		
			137 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			137 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| 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<byte> nameBytes)
 | |
|     {
 | |
|         Span<byte> namespaceBytes = stackalloc byte[16];
 | |
|         namespaceId.TryWriteBytes(namespaceBytes);
 | |
| 
 | |
|         Span<byte> buffer = stackalloc byte[namespaceBytes.Length + nameBytes.Length];
 | |
|         namespaceBytes.CopyTo(buffer);
 | |
|         nameBytes.CopyTo(buffer[namespaceBytes.Length..]);
 | |
| 
 | |
|         Span<byte> hash = stackalloc byte[32];
 | |
|         SHA256.TryHashData(buffer, hash, out _);
 | |
| 
 | |
|         Span<byte> 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<byte> hash)
 | |
|     {
 | |
|         Span<byte> 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);
 | |
|     }
 | |
| }
 |