notify doctors work, audit work, new product advisory sprints

This commit is contained in:
master
2026-01-13 08:36:29 +02:00
parent b8868a5f13
commit 9ca7cb183e
343 changed files with 24492 additions and 3544 deletions

View File

@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.Extensions.Logging;
@@ -414,23 +415,23 @@ public sealed class PrReachabilityGate : IPrReachabilityGate
var sb = new StringBuilder();
sb.AppendLine(passed
? "## Reachability Gate Passed"
: "## Reachability Gate Blocked");
? "## [OK] Reachability Gate Passed"
: "## [BLOCKED] Reachability Gate Blocked");
sb.AppendLine();
sb.AppendLine("| Metric | Value |");
sb.AppendLine("|--------|-------|");
sb.AppendLine($"| New reachable paths | {decision.NewReachableCount} |");
sb.AppendLine($"| New reachable paths | {decision.NewReachableCount.ToString(CultureInfo.InvariantCulture)} |");
if (options.IncludeMitigatedInSummary)
{
sb.AppendLine($"| Mitigated paths | {decision.MitigatedCount} |");
sb.AppendLine($"| Net change | {decision.NetChange:+#;-#;0} |");
sb.AppendLine($"| Mitigated paths | {decision.MitigatedCount.ToString(CultureInfo.InvariantCulture)} |");
sb.AppendLine($"| Net change | {decision.NetChange.ToString("+#;-#;0", CultureInfo.InvariantCulture)} |");
}
sb.AppendLine($"| Analysis type | {(decision.WasIncremental ? "Incremental" : "Full")} |");
sb.AppendLine($"| Cache savings | {decision.SavingsRatio:P0} |");
sb.AppendLine($"| Duration | {decision.Duration.TotalMilliseconds:F0}ms |");
sb.AppendLine($"| Cache savings | {decision.SavingsRatio.ToString("P0", CultureInfo.InvariantCulture)} |");
sb.AppendLine($"| Duration | {decision.Duration.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture)}ms |");
if (!passed && decision.BlockingFlips.Count > 0)
{
@@ -440,12 +441,13 @@ public sealed class PrReachabilityGate : IPrReachabilityGate
foreach (var flip in decision.BlockingFlips.Take(10))
{
sb.AppendLine($"- `{flip.EntryMethodKey}` -> `{flip.SinkMethodKey}` (confidence: {flip.Confidence:P0})");
sb.AppendLine($"- `{flip.EntryMethodKey}` -> `{flip.SinkMethodKey}` (confidence: {flip.Confidence.ToString("P0", CultureInfo.InvariantCulture)})");
}
if (decision.BlockingFlips.Count > 10)
{
sb.AppendLine($"- ... and {decision.BlockingFlips.Count - 10} more");
var remaining = decision.BlockingFlips.Count - 10;
sb.AppendLine($"- ... and {remaining.ToString(CultureInfo.InvariantCulture)} more");
}
}

View File

@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -124,7 +125,7 @@ public sealed class PathExplanationService : IPathExplanationService
}
/// <inheritdoc/>
public Task<ExplainedPath?> ExplainPathAsync(
public async Task<ExplainedPath?> ExplainPathAsync(
RichGraph graph,
string pathId,
CancellationToken cancellationToken = default)
@@ -145,20 +146,22 @@ public sealed class PathExplanationService : IPathExplanationService
MaxPaths = 100
};
var resultTask = ExplainAsync(graph, query, cancellationToken);
return resultTask.ContinueWith(t =>
var result = await ExplainAsync(graph, query, cancellationToken).ConfigureAwait(false);
if (result.Paths.Count == 0)
{
if (t.Result.Paths.Count == 0)
return null;
return null;
}
// If path index specified, return that specific one
if (parts.Length >= 3 && int.TryParse(parts[2], out var idx) && idx < t.Result.Paths.Count)
{
return t.Result.Paths[idx];
}
// If path index specified, return that specific one
if (parts.Length >= 3 &&
int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var idx) &&
idx >= 0 &&
idx < result.Paths.Count)
{
return result.Paths[idx];
}
return t.Result.Paths[0];
}, cancellationToken);
return result.Paths[0];
}
private static Dictionary<string, List<RichGraphEdge>> BuildEdgeLookup(RichGraph graph)

View File

@@ -4,6 +4,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.CallGraph;
using StellaOps.Scanner.Explainability.Assumptions;
@@ -11,6 +12,7 @@ using StellaOps.Scanner.Reachability.Binary;
using StellaOps.Scanner.Reachability.Runtime;
using StellaOps.Scanner.Reachability.Services;
using StellaOps.Scanner.Reachability.Stack;
using StellaOps.Determinism;
// Aliases to disambiguate types with same name in different namespaces
using StackEntrypointType = StellaOps.Scanner.Reachability.Stack.EntrypointType;
@@ -33,6 +35,7 @@ public sealed class ReachabilityEvidenceJobExecutor : IReachabilityEvidenceJobEx
private readonly IRuntimeReachabilityCollector? _runtimeCollector;
private readonly ILogger<ReachabilityEvidenceJobExecutor> _logger;
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
public ReachabilityEvidenceJobExecutor(
ICveSymbolMappingService cveSymbolService,
@@ -42,6 +45,7 @@ public sealed class ReachabilityEvidenceJobExecutor : IReachabilityEvidenceJobEx
ILogger<ReachabilityEvidenceJobExecutor> logger,
IBinaryPatchVerifier? binaryPatchVerifier = null,
IRuntimeReachabilityCollector? runtimeCollector = null,
IGuidProvider? guidProvider = null,
TimeProvider? timeProvider = null)
{
_cveSymbolService = cveSymbolService ?? throw new ArgumentNullException(nameof(cveSymbolService));
@@ -51,6 +55,7 @@ public sealed class ReachabilityEvidenceJobExecutor : IReachabilityEvidenceJobEx
_binaryPatchVerifier = binaryPatchVerifier;
_runtimeCollector = runtimeCollector;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
_timeProvider = timeProvider ?? TimeProvider.System;
}
@@ -409,7 +414,7 @@ public sealed class ReachabilityEvidenceJobExecutor : IReachabilityEvidenceJobEx
// Create a minimal stack with Unknown verdict
var unknownStack = new ReachabilityStack
{
Id = Guid.NewGuid().ToString("N"),
Id = _guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture),
FindingId = $"{job.CveId}:{job.Purl}",
Symbol = new StackVulnerableSymbol(
Name: "unknown",

View File

@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -22,7 +23,7 @@ public sealed class ReachabilityUnionWriter
private static readonly JsonWriterOptions JsonOptions = new()
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Encoder = JavaScriptEncoder.Default,
Indented = false,
SkipValidation = false
};

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -94,7 +95,7 @@ public static class RichGraphSemanticExtensions
return null;
}
return double.TryParse(value, out var score) ? score : null;
return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var score) ? score : null;
}
/// <summary>Gets the confidence score.</summary>
@@ -106,7 +107,7 @@ public static class RichGraphSemanticExtensions
return null;
}
return double.TryParse(value, out var score) ? score : null;
return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var score) ? score : null;
}
/// <summary>Checks if this node is an entrypoint.</summary>
@@ -190,13 +191,13 @@ public sealed class RichGraphNodeSemanticBuilder
public RichGraphNodeSemanticBuilder WithRiskScore(double score)
{
_attributes[RichGraphSemanticAttributes.RiskScore] = score.ToString("F3");
_attributes[RichGraphSemanticAttributes.RiskScore] = score.ToString("F3", CultureInfo.InvariantCulture);
return this;
}
public RichGraphNodeSemanticBuilder WithConfidence(double score, string tier)
{
_attributes[RichGraphSemanticAttributes.Confidence] = score.ToString("F3");
_attributes[RichGraphSemanticAttributes.Confidence] = score.ToString("F3", CultureInfo.InvariantCulture);
_attributes[RichGraphSemanticAttributes.ConfidenceTier] = tier;
return this;
}
@@ -225,7 +226,7 @@ public sealed class RichGraphNodeSemanticBuilder
public RichGraphNodeSemanticBuilder WithCweId(int cweId)
{
_attributes[RichGraphSemanticAttributes.CweId] = cweId.ToString();
_attributes[RichGraphSemanticAttributes.CweId] = cweId.ToString(CultureInfo.InvariantCulture);
return this;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -22,7 +23,7 @@ public sealed class RichGraphWriter
private static readonly JsonWriterOptions JsonOptions = new()
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Encoder = JavaScriptEncoder.Default,
Indented = false,
SkipValidation = false
};

View File

@@ -11,6 +11,7 @@ public sealed class InMemorySliceCache : ISliceCache, IDisposable
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
private readonly ILogger<InMemorySliceCache> _logger;
private readonly TimeProvider _timeProvider;
private readonly CancellationTokenSource _evictionCts = new();
private readonly Timer _evictionTimer;
private readonly SemaphoreSlim _evictionLock = new(1, 1);
@@ -26,7 +27,7 @@ public sealed class InMemorySliceCache : ISliceCache, IDisposable
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? TimeProvider.System;
_evictionTimer = new Timer(
_ => _ = EvictExpiredEntriesAsync(CancellationToken.None),
_ => _ = EvictExpiredEntriesAsync(_evictionCts.Token),
null,
TimeSpan.FromSeconds(EvictionIntervalSeconds),
TimeSpan.FromSeconds(EvictionIntervalSeconds));
@@ -116,7 +117,19 @@ public sealed class InMemorySliceCache : ISliceCache, IDisposable
private async Task EvictExpiredEntriesAsync(CancellationToken cancellationToken)
{
if (!await _evictionLock.WaitAsync(0, cancellationToken).ConfigureAwait(false))
if (cancellationToken.IsCancellationRequested)
{
return;
}
try
{
if (!await _evictionLock.WaitAsync(0, cancellationToken).ConfigureAwait(false))
{
return;
}
}
catch (OperationCanceledException)
{
return;
}
@@ -199,8 +212,14 @@ public sealed class InMemorySliceCache : ISliceCache, IDisposable
public void Dispose()
{
if (!_evictionCts.IsCancellationRequested)
{
_evictionCts.Cancel();
}
_evictionTimer?.Dispose();
_evictionLock?.Dispose();
_evictionCts.Dispose();
}
private sealed record CacheEntry(

View File

@@ -1,7 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) StellaOps
using System.Globalization;
using System.Text;
using StellaOps.Determinism;
using StellaOps.Scanner.Explainability.Assumptions;
namespace StellaOps.Scanner.Reachability.Stack;
@@ -48,6 +50,18 @@ public interface IReachabilityStackEvaluator
/// </remarks>
public sealed class ReachabilityStackEvaluator : IReachabilityStackEvaluator
{
private readonly IGuidProvider _guidProvider;
public ReachabilityStackEvaluator()
: this(SystemGuidProvider.Instance)
{
}
public ReachabilityStackEvaluator(IGuidProvider guidProvider)
{
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
}
/// <inheritdoc />
public ReachabilityStack Evaluate(
string findingId,
@@ -63,7 +77,7 @@ public sealed class ReachabilityStackEvaluator : IReachabilityStackEvaluator
return new ReachabilityStack
{
Id = Guid.NewGuid().ToString("N"),
Id = _guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture),
FindingId = findingId,
Symbol = symbol,
StaticCallGraph = layer1,

View File

@@ -26,6 +26,8 @@
<ProjectReference Include="..\..\..\Attestor\__Libraries\StellaOps.Attestor.GraphRoot\StellaOps.Attestor.GraphRoot.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
<ProjectReference Include="..\..\..\Signals\__Libraries\StellaOps.Signals.Ebpf\StellaOps.Signals.Ebpf.csproj" />
<ProjectReference Include="..\..\..\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Ghidra\StellaOps.BinaryIndex.Ghidra.csproj" />
<ProjectReference Include="..\..\..\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Decompiler\StellaOps.BinaryIndex.Decompiler.csproj" />

View File

@@ -1,8 +1,10 @@
// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later.
using System.Collections.Concurrent;
using System.Globalization;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor;
using StellaOps.Determinism;
namespace StellaOps.Scanner.Reachability;
@@ -16,17 +18,29 @@ public class SubgraphExtractor : IReachabilityResolver
private readonly IEntryPointResolver _entryPointResolver;
private readonly IVulnSurfaceService _vulnSurfaceService;
private readonly ILogger<SubgraphExtractor> _logger;
private readonly IGuidProvider _guidProvider;
public SubgraphExtractor(
IRichGraphStore graphStore,
IEntryPointResolver entryPointResolver,
IVulnSurfaceService vulnSurfaceService,
ILogger<SubgraphExtractor> logger)
: this(graphStore, entryPointResolver, vulnSurfaceService, logger, SystemGuidProvider.Instance)
{
}
public SubgraphExtractor(
IRichGraphStore graphStore,
IEntryPointResolver entryPointResolver,
IVulnSurfaceService vulnSurfaceService,
ILogger<SubgraphExtractor> logger,
IGuidProvider guidProvider)
{
_graphStore = graphStore ?? throw new ArgumentNullException(nameof(graphStore));
_entryPointResolver = entryPointResolver ?? throw new ArgumentNullException(nameof(entryPointResolver));
_vulnSurfaceService = vulnSurfaceService ?? throw new ArgumentNullException(nameof(vulnSurfaceService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_guidProvider = guidProvider ?? throw new ArgumentNullException(nameof(guidProvider));
}
public async Task<PoESubgraph?> ResolveAsync(
@@ -206,7 +220,7 @@ public class SubgraphExtractor : IReachabilityResolver
if (sinkSet.Contains(current))
{
paths.Add(new CallPath(
PathId: Guid.NewGuid().ToString(),
PathId: _guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture),
Nodes: path.ToList(),
Edges: ExtractEdgesFromPath(path, graph),
Length: path.Count - 1,

View File

@@ -1,6 +1,8 @@
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Attestor.Envelope;
using StellaOps.Canonical.Json;
namespace StellaOps.Scanner.Reachability.Witnesses;
@@ -11,11 +13,12 @@ namespace StellaOps.Scanner.Reachability.Witnesses;
public sealed class SuppressionDsseSigner : ISuppressionDsseSigner
{
private readonly EnvelopeSignatureService _signatureService;
private static readonly JsonSerializerOptions CanonicalJsonOptions = new(JsonSerializerDefaults.Web)
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.Default
};
/// <summary>
@@ -44,13 +47,10 @@ public sealed class SuppressionDsseSigner : ISuppressionDsseSigner
try
{
// Serialize witness to canonical JSON bytes
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(witness, CanonicalJsonOptions);
var payloadBytes = CanonJson.Canonicalize(witness, CanonicalJsonOptions);
// Build the PAE (Pre-Authentication Encoding) for DSSE
var pae = BuildPae(SuppressionWitnessSchema.DssePayloadType, payloadBytes);
// Sign the PAE
var signResult = _signatureService.Sign(pae, signingKey, cancellationToken);
// Sign DSSE payload using the shared PAE helper
var signResult = _signatureService.SignDsse(SuppressionWitnessSchema.DssePayloadType, payloadBytes, signingKey, cancellationToken);
if (!signResult.IsSuccess)
{
return SuppressionDsseResult.Failure($"Signing failed: {signResult.Error?.Message}");
@@ -114,12 +114,10 @@ public sealed class SuppressionDsseSigner : ISuppressionDsseSigner
return SuppressionVerifyResult.Failure($"No signature found for key ID: {publicKey.KeyId}");
}
// Build PAE and verify signature
var pae = BuildPae(envelope.PayloadType, envelope.Payload.ToArray());
var signatureBytes = Convert.FromBase64String(matchingSignature.Signature);
var envelopeSignature = new EnvelopeSignature(publicKey.KeyId, publicKey.AlgorithmId, signatureBytes);
var verifyResult = _signatureService.Verify(pae, envelopeSignature, publicKey, cancellationToken);
var verifyResult = _signatureService.VerifyDsse(envelope.PayloadType, envelope.Payload.Span, envelopeSignature, publicKey, cancellationToken);
if (!verifyResult.IsSuccess)
{
return SuppressionVerifyResult.Failure($"Signature verification failed: {verifyResult.Error?.Message}");
@@ -133,43 +131,6 @@ public sealed class SuppressionDsseSigner : ISuppressionDsseSigner
}
}
/// <summary>
/// Builds the DSSE Pre-Authentication Encoding (PAE) for a payload.
/// PAE = "DSSEv1" SP len(type) SP type SP len(payload) SP payload
/// </summary>
private static byte[] BuildPae(string payloadType, byte[] payload)
{
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
// Write "DSSEv1 "
writer.Write(Encoding.UTF8.GetBytes("DSSEv1 "));
// Write len(type) as ASCII decimal string followed by space
WriteLengthAndSpace(writer, typeBytes.Length);
// Write type followed by space
writer.Write(typeBytes);
writer.Write((byte)' ');
// Write len(payload) as ASCII decimal string followed by space
WriteLengthAndSpace(writer, payload.Length);
// Write payload
writer.Write(payload);
writer.Flush();
return stream.ToArray();
}
private static void WriteLengthAndSpace(BinaryWriter writer, int length)
{
// Write length as ASCII decimal string
writer.Write(Encoding.UTF8.GetBytes(length.ToString()));
writer.Write((byte)' ');
}
}
/// <summary>

View File

@@ -1,6 +1,8 @@
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Attestor.Envelope;
using StellaOps.Canonical.Json;
namespace StellaOps.Scanner.Reachability.Witnesses;
@@ -11,11 +13,12 @@ namespace StellaOps.Scanner.Reachability.Witnesses;
public sealed class WitnessDsseSigner : IWitnessDsseSigner
{
private readonly EnvelopeSignatureService _signatureService;
private static readonly JsonSerializerOptions CanonicalJsonOptions = new(JsonSerializerDefaults.Web)
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.Default
};
/// <summary>
@@ -44,13 +47,10 @@ public sealed class WitnessDsseSigner : IWitnessDsseSigner
try
{
// Serialize witness to canonical JSON bytes
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(witness, CanonicalJsonOptions);
var payloadBytes = CanonJson.Canonicalize(witness, CanonicalJsonOptions);
// Build the PAE (Pre-Authentication Encoding) for DSSE
var pae = BuildPae(WitnessSchema.DssePayloadType, payloadBytes);
// Sign the PAE
var signResult = _signatureService.Sign(pae, signingKey, cancellationToken);
// Sign DSSE payload using the shared PAE helper
var signResult = _signatureService.SignDsse(WitnessSchema.DssePayloadType, payloadBytes, signingKey, cancellationToken);
if (!signResult.IsSuccess)
{
return WitnessDsseResult.Failure($"Signing failed: {signResult.Error?.Message}");
@@ -114,12 +114,10 @@ public sealed class WitnessDsseSigner : IWitnessDsseSigner
return WitnessVerifyResult.Failure($"No signature found for key ID: {publicKey.KeyId}");
}
// Build PAE and verify signature
var pae = BuildPae(envelope.PayloadType, envelope.Payload.ToArray());
var signatureBytes = Convert.FromBase64String(matchingSignature.Signature);
var envelopeSignature = new EnvelopeSignature(publicKey.KeyId, publicKey.AlgorithmId, signatureBytes);
var verifyResult = _signatureService.Verify(pae, envelopeSignature, publicKey, cancellationToken);
var verifyResult = _signatureService.VerifyDsse(envelope.PayloadType, envelope.Payload.Span, envelopeSignature, publicKey, cancellationToken);
if (!verifyResult.IsSuccess)
{
return WitnessVerifyResult.Failure($"Signature verification failed: {verifyResult.Error?.Message}");
@@ -133,43 +131,6 @@ public sealed class WitnessDsseSigner : IWitnessDsseSigner
}
}
/// <summary>
/// Builds the DSSE Pre-Authentication Encoding (PAE) for a payload.
/// PAE = "DSSEv1" SP len(type) SP type SP len(payload) SP payload
/// </summary>
private static byte[] BuildPae(string payloadType, byte[] payload)
{
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
// Write "DSSEv1 "
writer.Write(Encoding.UTF8.GetBytes("DSSEv1 "));
// Write len(type) as little-endian 8-byte integer followed by space
WriteLengthAndSpace(writer, typeBytes.Length);
// Write type followed by space
writer.Write(typeBytes);
writer.Write((byte)' ');
// Write len(payload) as little-endian 8-byte integer followed by space
WriteLengthAndSpace(writer, payload.Length);
// Write payload
writer.Write(payload);
writer.Flush();
return stream.ToArray();
}
private static void WriteLengthAndSpace(BinaryWriter writer, int length)
{
// Write length as ASCII decimal string
writer.Write(Encoding.UTF8.GetBytes(length.ToString()));
writer.Write((byte)' ');
}
}
/// <summary>