Add unit tests for RabbitMq and Udp transport servers and clients
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling. - Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options. - Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation. - Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios. - Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling. - Included tests for UdpTransportOptions to verify default values and modification capabilities. - Enhanced service registration tests for Udp transport services in the dependency injection container.
This commit is contained in:
@@ -8,8 +8,15 @@ namespace StellaOps.Scanner.Reachability;
|
||||
/// Builds canonical CodeIDs used by richgraph-v1 to anchor symbols when names are missing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Format: <c>code:<lang>:<base64url-sha256></c> where the hash is computed over a
|
||||
/// <para>
|
||||
/// Format: <c>code:{lang}:{base64url-sha256}</c> where the hash is computed over a
|
||||
/// canonical tuple that is stable across machines and paths.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>INTEROP NOTE:</strong> This static class uses SHA-256 for maximum external tool
|
||||
/// compatibility. For compliance-profile-aware code IDs that respect GOST/SM3/FIPS profiles,
|
||||
/// use <see cref="CodeIdBuilder"/> with an injected <see cref="StellaOps.Cryptography.ICryptoHash"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class CodeId
|
||||
{
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability;
|
||||
|
||||
/// <summary>
|
||||
/// Builds canonical CodeIDs with compliance-profile-aware hashing.
|
||||
/// Uses <see cref="HashPurpose.Symbol"/> which resolves to:
|
||||
/// - SHA-256 for "world" and "fips" profiles
|
||||
/// - GOST3411-2012-256 for "gost" profile
|
||||
/// - SM3 for "sm" profile
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Format: <c>code:{lang}:{base64url-hash}</c> where the hash is computed over a
|
||||
/// canonical tuple that is stable across machines and paths.
|
||||
/// </remarks>
|
||||
public sealed class CodeIdBuilder
|
||||
{
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new CodeIdBuilder with the specified crypto hash service.
|
||||
/// </summary>
|
||||
/// <param name="cryptoHash">Crypto hash service for compliance-aware hashing.</param>
|
||||
public CodeIdBuilder(ICryptoHash cryptoHash)
|
||||
{
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a binary code-id from binary components.
|
||||
/// </summary>
|
||||
public string ForBinary(string buildId, string section, string? relativePath)
|
||||
{
|
||||
var tuple = $"{Norm(buildId)}\0{Norm(section)}\0{Norm(relativePath)}";
|
||||
return Build("binary", tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a .NET code-id from assembly components.
|
||||
/// </summary>
|
||||
public string ForDotNet(string assemblyName, string moduleName, string? mvid)
|
||||
{
|
||||
var tuple = $"{Norm(assemblyName)}\0{Norm(moduleName)}\0{Norm(mvid)}";
|
||||
return Build("dotnet", tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a binary code-id using canonical address + length tuple.
|
||||
/// </summary>
|
||||
public string ForBinarySegment(string format, string fileHash, string address, long? lengthBytes = null, string? section = null, string? codeBlockHash = null)
|
||||
{
|
||||
var tuple = $"{Norm(format)}\0{Norm(fileHash)}\0{NormalizeAddress(address)}\0{NormalizeLength(lengthBytes)}\0{Norm(section)}\0{Norm(codeBlockHash)}";
|
||||
return Build("binary", tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Node code-id from package components.
|
||||
/// </summary>
|
||||
public string ForNode(string packageName, string entryPath)
|
||||
{
|
||||
var tuple = $"{Norm(packageName)}\0{Norm(entryPath)}";
|
||||
return Build("node", tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a code-id from an existing symbol ID.
|
||||
/// </summary>
|
||||
public string FromSymbolId(string symbolId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(symbolId);
|
||||
return Build("sym", symbolId.Trim());
|
||||
}
|
||||
|
||||
private string Build(string lang, string tuple)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(tuple);
|
||||
var hash = _cryptoHash.ComputeHashForPurpose(bytes, HashPurpose.Symbol);
|
||||
var base64 = Convert.ToBase64String(hash)
|
||||
.TrimEnd('=')
|
||||
.Replace('+', '-')
|
||||
.Replace('/', '_');
|
||||
return $"code:{lang}:{base64}";
|
||||
}
|
||||
|
||||
private static string NormalizeAddress(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "0x0";
|
||||
}
|
||||
|
||||
var addrText = value.Trim();
|
||||
var isHex = addrText.StartsWith("0x", StringComparison.OrdinalIgnoreCase);
|
||||
if (isHex)
|
||||
{
|
||||
addrText = addrText[2..];
|
||||
}
|
||||
|
||||
if (long.TryParse(addrText, isHex ? System.Globalization.NumberStyles.HexNumber : System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var addrValue))
|
||||
{
|
||||
if (addrValue < 0)
|
||||
{
|
||||
addrValue = 0;
|
||||
}
|
||||
|
||||
return $"0x{addrValue:x}";
|
||||
}
|
||||
|
||||
addrText = addrText.TrimStart('0');
|
||||
if (addrText.Length == 0)
|
||||
{
|
||||
addrText = "0";
|
||||
}
|
||||
|
||||
return $"0x{addrText.ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static string NormalizeLength(long? value)
|
||||
{
|
||||
if (value is null or <= 0)
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
return value.Value.ToString("D", System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string Norm(string? value) => (value ?? string.Empty).Trim();
|
||||
}
|
||||
@@ -1,20 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability;
|
||||
|
||||
/// <summary>
|
||||
/// Writes richgraph-v1 documents to disk with canonical ordering and BLAKE3 hash.
|
||||
/// Writes richgraph-v1 documents to disk with canonical ordering and compliance-profile-aware hashing.
|
||||
/// Uses <see cref="HashPurpose.Graph"/> for content addressing, which resolves to:
|
||||
/// - BLAKE3-256 for "world" profile
|
||||
/// - SHA-256 for "fips" profile
|
||||
/// - GOST3411-2012-256 for "gost" profile
|
||||
/// - SM3 for "sm" profile
|
||||
/// </summary>
|
||||
public sealed class RichGraphWriter
|
||||
{
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
|
||||
private static readonly JsonWriterOptions JsonOptions = new()
|
||||
{
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
@@ -22,6 +26,15 @@ public sealed class RichGraphWriter
|
||||
SkipValidation = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RichGraphWriter with the specified crypto hash service.
|
||||
/// </summary>
|
||||
/// <param name="cryptoHash">Crypto hash service for compliance-aware hashing.</param>
|
||||
public RichGraphWriter(ICryptoHash cryptoHash)
|
||||
{
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
}
|
||||
|
||||
public async Task<RichGraphWriteResult> WriteAsync(
|
||||
RichGraph graph,
|
||||
string outputRoot,
|
||||
@@ -46,7 +59,7 @@ public sealed class RichGraphWriter
|
||||
}
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(graphPath, cancellationToken).ConfigureAwait(false);
|
||||
var graphHash = ComputeSha256(bytes);
|
||||
var graphHash = _cryptoHash.ComputePrefixedHashForPurpose(bytes, HashPurpose.Graph);
|
||||
|
||||
var metaPath = Path.Combine(root, "meta.json");
|
||||
await using (var stream = File.Create(metaPath))
|
||||
@@ -169,12 +182,6 @@ public sealed class RichGraphWriter
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static string ComputeSha256(IReadOnlyList<byte> bytes)
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
var hash = sha.ComputeHash(bytes.ToArray());
|
||||
return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record RichGraphWriteResult(
|
||||
|
||||
@@ -9,5 +9,6 @@
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Surface.Env\StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Scanner.Analyzers.Native\StellaOps.Scanner.Analyzers.Native.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -10,8 +10,15 @@ namespace StellaOps.Scanner.Reachability;
|
||||
/// to remain reproducible and cacheable across hosts.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Format: <c>sym:{lang}:{stable-fragment}</c>
|
||||
/// where stable-fragment is SHA-256(base64url-no-pad) of the canonical tuple per language.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>INTEROP NOTE:</strong> This static class uses SHA-256 for maximum external tool
|
||||
/// compatibility. For compliance-profile-aware symbol IDs that respect GOST/SM3/FIPS profiles,
|
||||
/// use <see cref="SymbolIdBuilder"/> with an injected <see cref="StellaOps.Cryptography.ICryptoHash"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class SymbolId
|
||||
{
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability;
|
||||
|
||||
/// <summary>
|
||||
/// Builds canonical SymbolIDs with compliance-profile-aware hashing.
|
||||
/// Uses <see cref="HashPurpose.Symbol"/> which resolves to:
|
||||
/// - SHA-256 for "world" and "fips" profiles
|
||||
/// - GOST3411-2012-256 for "gost" profile
|
||||
/// - SM3 for "sm" profile
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Format: <c>sym:{lang}:{stable-fragment}</c>
|
||||
/// where stable-fragment is base64url-no-pad of the profile-appropriate hash of the canonical tuple.
|
||||
/// </remarks>
|
||||
public sealed class SymbolIdBuilder
|
||||
{
|
||||
private readonly ICryptoHash _cryptoHash;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new SymbolIdBuilder with the specified crypto hash service.
|
||||
/// </summary>
|
||||
/// <param name="cryptoHash">Crypto hash service for compliance-aware hashing.</param>
|
||||
public SymbolIdBuilder(ICryptoHash cryptoHash)
|
||||
{
|
||||
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Java symbol ID from method signature components.
|
||||
/// </summary>
|
||||
public string ForJava(string package, string className, string method, string descriptor)
|
||||
{
|
||||
var tuple = $"{Lower(package)}\0{Lower(className)}\0{Lower(method)}\0{Lower(descriptor)}";
|
||||
return Build(SymbolId.Lang.Java, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a .NET symbol ID from member signature components.
|
||||
/// </summary>
|
||||
public string ForDotNet(string assemblyName, string ns, string typeName, string memberSignature)
|
||||
{
|
||||
var tuple = $"{Norm(assemblyName)}\0{Norm(ns)}\0{Norm(typeName)}\0{Norm(memberSignature)}";
|
||||
return Build(SymbolId.Lang.DotNet, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Node/Deno symbol ID from module export components.
|
||||
/// </summary>
|
||||
public string ForNode(string pkgNameOrPath, string exportPath, string kind)
|
||||
{
|
||||
var tuple = $"{Norm(pkgNameOrPath)}\0{Norm(exportPath)}\0{Norm(kind)}";
|
||||
return Build(SymbolId.Lang.Node, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Deno symbol ID from module export components.
|
||||
/// </summary>
|
||||
public string ForDeno(string pkgNameOrPath, string exportPath, string kind)
|
||||
{
|
||||
var tuple = $"{Norm(pkgNameOrPath)}\0{Norm(exportPath)}\0{Norm(kind)}";
|
||||
return Build(SymbolId.Lang.Deno, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Go symbol ID from function/method components.
|
||||
/// </summary>
|
||||
public string ForGo(string modulePath, string packagePath, string receiver, string func)
|
||||
{
|
||||
var tuple = $"{Norm(modulePath)}\0{Norm(packagePath)}\0{Norm(receiver)}\0{Norm(func)}";
|
||||
return Build(SymbolId.Lang.Go, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Rust symbol ID from item components.
|
||||
/// </summary>
|
||||
public string ForRust(string crateName, string modulePath, string itemName, string? mangled = null)
|
||||
{
|
||||
var tuple = $"{Norm(crateName)}\0{Norm(modulePath)}\0{Norm(itemName)}\0{Norm(mangled)}";
|
||||
return Build(SymbolId.Lang.Rust, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Swift symbol ID from member components.
|
||||
/// </summary>
|
||||
public string ForSwift(string module, string typeName, string member, string? mangled = null)
|
||||
{
|
||||
var tuple = $"{Norm(module)}\0{Norm(typeName)}\0{Norm(member)}\0{Norm(mangled)}";
|
||||
return Build(SymbolId.Lang.Swift, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a shell symbol ID from script/function components.
|
||||
/// </summary>
|
||||
public string ForShell(string scriptRelPath, string functionOrCmd)
|
||||
{
|
||||
var tuple = $"{Norm(scriptRelPath)}\0{Norm(functionOrCmd)}";
|
||||
return Build(SymbolId.Lang.Shell, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a binary symbol ID from ELF/PE/Mach-O components.
|
||||
/// </summary>
|
||||
public string ForBinary(string buildId, string section, string symbolName)
|
||||
=> ForBinaryAddressed(buildId, section, string.Empty, symbolName, "static", null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a binary symbol ID that includes file hash, section, address, and linkage.
|
||||
/// </summary>
|
||||
public string ForBinaryAddressed(string fileHash, string section, string address, string symbolName, string linkage, string? codeBlockHash = null)
|
||||
{
|
||||
var tuple = $"{Norm(fileHash)}\0{Norm(section)}\0{NormalizeAddress(address)}\0{Norm(symbolName)}\0{Norm(linkage)}\0{Norm(codeBlockHash)}";
|
||||
return Build(SymbolId.Lang.Binary, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Python symbol ID from module/function components.
|
||||
/// </summary>
|
||||
public string ForPython(string packageOrPath, string modulePath, string qualifiedName)
|
||||
{
|
||||
var tuple = $"{Norm(packageOrPath)}\0{Norm(modulePath)}\0{Norm(qualifiedName)}";
|
||||
return Build(SymbolId.Lang.Python, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Ruby symbol ID from module/method components.
|
||||
/// </summary>
|
||||
public string ForRuby(string gemOrPath, string modulePath, string methodName)
|
||||
{
|
||||
var tuple = $"{Norm(gemOrPath)}\0{Norm(modulePath)}\0{Norm(methodName)}";
|
||||
return Build(SymbolId.Lang.Ruby, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PHP symbol ID from namespace/function components.
|
||||
/// </summary>
|
||||
public string ForPhp(string composerPackage, string ns, string qualifiedName)
|
||||
{
|
||||
var tuple = $"{Norm(composerPackage)}\0{Norm(ns)}\0{Norm(qualifiedName)}";
|
||||
return Build(SymbolId.Lang.Php, tuple);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a symbol ID from a pre-computed canonical tuple and language.
|
||||
/// </summary>
|
||||
public string FromTuple(string lang, string canonicalTuple)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(lang);
|
||||
return Build(lang, canonicalTuple);
|
||||
}
|
||||
|
||||
private string Build(string lang, string tuple)
|
||||
{
|
||||
var hash = ComputeFragment(tuple);
|
||||
return $"sym:{lang}:{hash}";
|
||||
}
|
||||
|
||||
private string ComputeFragment(string tuple)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(tuple);
|
||||
var hash = _cryptoHash.ComputeHashForPurpose(bytes, HashPurpose.Symbol);
|
||||
// Base64url without padding per spec
|
||||
return Convert.ToBase64String(hash)
|
||||
.Replace('+', '-')
|
||||
.Replace('/', '_')
|
||||
.TrimEnd('=');
|
||||
}
|
||||
|
||||
private static string NormalizeAddress(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "0x0";
|
||||
}
|
||||
|
||||
var addrText = value.Trim();
|
||||
var isHex = addrText.StartsWith("0x", StringComparison.OrdinalIgnoreCase);
|
||||
if (isHex)
|
||||
{
|
||||
addrText = addrText[2..];
|
||||
}
|
||||
|
||||
if (long.TryParse(addrText, isHex ? System.Globalization.NumberStyles.HexNumber : System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var addrValue))
|
||||
{
|
||||
if (addrValue < 0)
|
||||
{
|
||||
addrValue = 0;
|
||||
}
|
||||
|
||||
return $"0x{addrValue:x}";
|
||||
}
|
||||
|
||||
addrText = addrText.TrimStart('0');
|
||||
if (addrText.Length == 0)
|
||||
{
|
||||
addrText = "0";
|
||||
}
|
||||
|
||||
return $"0x{addrText.ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static string Lower(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToLowerInvariant();
|
||||
|
||||
private static string Norm(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
|
||||
}
|
||||
Reference in New Issue
Block a user