up
	
		
			
	
		
	
	
		
	
		
			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
				
			
		
		
	
	
				
					
				
			
		
			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
				
			This commit is contained in:
		
							
								
								
									
										136
									
								
								src/StellaOps.Scanner.Core/Utility/ScannerIdentifiers.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/StellaOps.Scanner.Core/Utility/ScannerIdentifiers.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| 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); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user