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

@@ -426,8 +426,13 @@ function flush() {
const sorted = events.sort((a, b) => {
const at = String(a.ts);
const bt = String(b.ts);
if (at === bt) return String(a.type).localeCompare(String(b.type));
return at.localeCompare(bt);
if (at === bt) {
const atype = String(a.type);
const btype = String(b.type);
if (atype === btype) return 0;
return atype < btype ? -1 : 1;
}
return at < bt ? -1 : 1;
});
const data = sorted.map((e) => JSON.stringify(e)).join("\\n");

View File

@@ -11,11 +11,12 @@ internal sealed class DenoRuntimeTraceRecorder
private readonly string _rootPath;
private readonly TimeProvider _timeProvider;
public DenoRuntimeTraceRecorder(string rootPath, TimeProvider? timeProvider = null)
public DenoRuntimeTraceRecorder(string rootPath, TimeProvider timeProvider)
{
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
ArgumentNullException.ThrowIfNull(timeProvider);
_rootPath = Path.GetFullPath(rootPath);
_timeProvider = timeProvider ?? TimeProvider.System;
_timeProvider = timeProvider;
}
public void AddModuleLoad(string absoluteModulePath, string reason, IEnumerable<string> permissions, string? origin = null, DateTimeOffset? timestamp = null)

View File

@@ -13,6 +13,12 @@ internal static class DenoRuntimeTraceRunner
private const string EntrypointEnvVar = "STELLA_DENO_ENTRYPOINT";
private const string BinaryEnvVar = "STELLA_DENO_BINARY";
private const string RuntimeFileName = "deno-runtime.ndjson";
private static readonly HashSet<string> AllowedBinaryNames = new(StringComparer.OrdinalIgnoreCase)
{
"deno",
"deno.exe",
"deno.cmd"
};
public static async Task<bool> TryExecuteAsync(
LanguageAnalyzerContext context,
@@ -28,29 +34,28 @@ internal static class DenoRuntimeTraceRunner
return false;
}
var entrypointPath = Path.GetFullPath(Path.Combine(context.RootPath, entrypoint));
if (!File.Exists(entrypointPath))
var rootPath = Path.GetFullPath(context.RootPath);
if (!TryResolveEntrypointPath(rootPath, entrypoint, logger, out var entrypointPath))
{
logger?.LogWarning("Deno runtime trace skipped: entrypoint '{Entrypoint}' missing", entrypointPath);
return false;
}
var shimPath = Path.Combine(context.RootPath, DenoRuntimeShim.FileName);
if (!File.Exists(shimPath))
{
await DenoRuntimeShim.WriteAsync(context.RootPath, cancellationToken).ConfigureAwait(false);
}
var binary = Environment.GetEnvironmentVariable(BinaryEnvVar);
var binary = ResolveBinary(rootPath, Environment.GetEnvironmentVariable(BinaryEnvVar), logger);
if (string.IsNullOrWhiteSpace(binary))
{
binary = "deno";
return false;
}
var shimPath = Path.Combine(rootPath, DenoRuntimeShim.FileName);
if (!File.Exists(shimPath))
{
await DenoRuntimeShim.WriteAsync(rootPath, cancellationToken).ConfigureAwait(false);
}
var startInfo = new ProcessStartInfo
{
FileName = binary,
WorkingDirectory = context.RootPath,
WorkingDirectory = rootPath,
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false,
@@ -58,7 +63,7 @@ internal static class DenoRuntimeTraceRunner
startInfo.ArgumentList.Add("run");
startInfo.ArgumentList.Add("--cached-only");
startInfo.ArgumentList.Add("--allow-read");
startInfo.ArgumentList.Add(BuildAllowReadArgument(rootPath, logger));
startInfo.ArgumentList.Add("--allow-env");
startInfo.ArgumentList.Add("--quiet");
startInfo.ArgumentList.Add(shimPath);
@@ -96,7 +101,7 @@ internal static class DenoRuntimeTraceRunner
return false;
}
var runtimePath = Path.Combine(context.RootPath, RuntimeFileName);
var runtimePath = Path.Combine(rootPath, RuntimeFileName);
if (!File.Exists(runtimePath))
{
logger?.LogWarning(
@@ -108,6 +113,122 @@ internal static class DenoRuntimeTraceRunner
return true;
}
private static bool TryResolveEntrypointPath(
string rootPath,
string entrypoint,
ILogger? logger,
out string entrypointPath)
{
entrypointPath = string.Empty;
if (string.IsNullOrWhiteSpace(entrypoint))
{
logger?.LogWarning("Deno runtime trace skipped: entrypoint was empty");
return false;
}
try
{
var candidate = Path.GetFullPath(Path.Combine(rootPath, entrypoint));
if (!IsWithinRoot(rootPath, candidate))
{
logger?.LogWarning("Deno runtime trace skipped: entrypoint '{Entrypoint}' not under root", entrypoint);
return false;
}
if (!File.Exists(candidate))
{
logger?.LogWarning("Deno runtime trace skipped: entrypoint '{Entrypoint}' missing", candidate);
return false;
}
entrypointPath = candidate;
return true;
}
catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or PathTooLongException or UnauthorizedAccessException)
{
logger?.LogWarning(ex, "Deno runtime trace skipped: entrypoint '{Entrypoint}' invalid", entrypoint);
return false;
}
}
private static string? ResolveBinary(string rootPath, string? binary, ILogger? logger)
{
if (string.IsNullOrWhiteSpace(binary))
{
return "deno";
}
var trimmed = binary.Trim();
var fileName = Path.GetFileName(trimmed);
if (string.IsNullOrWhiteSpace(fileName) || !AllowedBinaryNames.Contains(fileName))
{
logger?.LogWarning("Deno runtime trace skipped: binary '{Binary}' not allowlisted", trimmed);
return null;
}
var isPath = trimmed.Contains(Path.DirectorySeparatorChar) ||
trimmed.Contains(Path.AltDirectorySeparatorChar) ||
Path.IsPathRooted(trimmed);
if (!isPath)
{
return trimmed;
}
try
{
var candidate = Path.GetFullPath(Path.IsPathRooted(trimmed)
? trimmed
: Path.Combine(rootPath, trimmed));
if (!IsWithinRoot(rootPath, candidate))
{
logger?.LogWarning("Deno runtime trace skipped: binary '{Binary}' not under root", trimmed);
return null;
}
if (!File.Exists(candidate))
{
logger?.LogWarning("Deno runtime trace skipped: binary '{Binary}' missing", candidate);
return null;
}
return candidate;
}
catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or PathTooLongException or UnauthorizedAccessException)
{
logger?.LogWarning(ex, "Deno runtime trace skipped: binary '{Binary}' invalid", trimmed);
return null;
}
}
private static string BuildAllowReadArgument(string rootPath, ILogger? logger)
{
var comparison = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
var allowed = new HashSet<string>(comparison) { rootPath };
var denoDir = Environment.GetEnvironmentVariable("DENO_DIR");
if (!string.IsNullOrWhiteSpace(denoDir))
{
try
{
var denoDirPath = Path.GetFullPath(denoDir, rootPath);
allowed.Add(denoDirPath);
}
catch (Exception ex) when (ex is ArgumentException or IOException or NotSupportedException or PathTooLongException or UnauthorizedAccessException)
{
logger?.LogWarning(ex, "Deno runtime trace: invalid DENO_DIR '{DenoDir}'", denoDir);
}
}
var ordered = allowed.OrderBy(path => path, comparison);
return $"--allow-read={string.Join(",", ordered)}";
}
private static bool IsWithinRoot(string rootPath, string candidatePath)
{
var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
var normalizedRoot = rootPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
return candidatePath.StartsWith(normalizedRoot, comparison);
}
private static string Truncate(string? value, int maxLength = 400)
{
if (string.IsNullOrEmpty(value))

View File

@@ -1,5 +1,6 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
@@ -8,7 +9,7 @@ internal static class DenoRuntimeTraceSerializer
{
private static readonly JsonWriterOptions WriterOptions = new()
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Encoder = JavaScriptEncoder.Default,
Indented = false
};

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Globalization;
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal.Bundling;
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
@@ -278,7 +279,7 @@ internal sealed record BundlingSignal(
yield return new("bundle.detected", "true");
yield return new("bundle.filePath", FilePath);
yield return new("bundle.kind", Kind.ToString().ToLowerInvariant());
yield return new("bundle.sizeBytes", SizeBytes.ToString());
yield return new("bundle.sizeBytes", SizeBytes.ToString(CultureInfo.InvariantCulture));
if (IsSkipped)
{
@@ -292,7 +293,7 @@ internal sealed record BundlingSignal(
{
if (EstimatedBundledAssemblies > 0)
{
yield return new("bundle.estimatedAssemblies", EstimatedBundledAssemblies.ToString());
yield return new("bundle.estimatedAssemblies", EstimatedBundledAssemblies.ToString(CultureInfo.InvariantCulture));
}
for (var i = 0; i < Indicators.Length; i++)

View File

@@ -23,10 +23,10 @@ internal sealed class DotNetCallgraphBuilder
private int _assemblyCount;
private int _typeCount;
public DotNetCallgraphBuilder(string contextDigest, TimeProvider? timeProvider = null)
public DotNetCallgraphBuilder(string contextDigest, TimeProvider timeProvider)
{
_contextDigest = contextDigest;
_timeProvider = timeProvider ?? TimeProvider.System;
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
/// <summary>

View File

@@ -232,6 +232,7 @@ internal static class DotNetLicenseCache
DtdProcessing = DtdProcessing.Ignore,
IgnoreComments = true,
IgnoreWhitespace = true,
XmlResolver = null,
});
var expressions = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);

View File

@@ -0,0 +1,10 @@
# Scanner .NET Analyzer Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md` and `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-HOTLIST-SCANNER-LANG-DOTNET-0001 | DONE | Applied hotlist fixes and tests. |
| AUDIT-0644-A | DONE | Audit tracker updated for DotNet analyzer apply. |
| AUDIT-0698-A | DONE | Test project apply completed (warnings, deterministic fixtures). |

View File

@@ -18,10 +18,11 @@ internal sealed class NativeCallgraphBuilder
private readonly TimeProvider _timeProvider;
private int _binaryCount;
public NativeCallgraphBuilder(string layerDigest, TimeProvider? timeProvider = null)
public NativeCallgraphBuilder(string layerDigest, TimeProvider timeProvider)
{
_layerDigest = layerDigest;
_timeProvider = timeProvider ?? TimeProvider.System;
ArgumentNullException.ThrowIfNull(timeProvider);
_timeProvider = timeProvider;
}
/// <summary>

View File

@@ -16,6 +16,14 @@ namespace StellaOps.Scanner.Analyzers.Native;
/// </summary>
public sealed class NativeReachabilityAnalyzer
{
private readonly TimeProvider _timeProvider;
public NativeReachabilityAnalyzer(TimeProvider timeProvider)
{
ArgumentNullException.ThrowIfNull(timeProvider);
_timeProvider = timeProvider;
}
/// <summary>
/// Analyzes a directory of ELF binaries and produces a reachability graph.
/// </summary>
@@ -31,7 +39,7 @@ public sealed class NativeReachabilityAnalyzer
ArgumentException.ThrowIfNullOrEmpty(layerPath);
ArgumentException.ThrowIfNullOrEmpty(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest, _timeProvider);
// Find all potential ELF files in the layer
await foreach (var filePath in FindElfFilesAsync(layerPath, cancellationToken))
@@ -73,7 +81,7 @@ public sealed class NativeReachabilityAnalyzer
ArgumentException.ThrowIfNullOrEmpty(filePath);
ArgumentException.ThrowIfNullOrEmpty(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest, _timeProvider);
await using var stream = File.OpenRead(filePath);
var elf = ElfReader.Parse(stream, filePath, layerDigest);
@@ -98,7 +106,7 @@ public sealed class NativeReachabilityAnalyzer
ArgumentException.ThrowIfNullOrEmpty(filePath);
ArgumentException.ThrowIfNullOrEmpty(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest);
var builder = new NativeCallgraphBuilder(layerDigest, _timeProvider);
var elf = ElfReader.Parse(stream, filePath, layerDigest);
if (elf is not null)

View File

@@ -1,3 +1,5 @@
using System.Globalization;
namespace StellaOps.Scanner.Analyzers.Native.RuntimeCapture.Timeline;
public interface ITimelineBuilder
@@ -108,7 +110,7 @@ public sealed class TimelineBuilder : ITimelineBuilder
Details = new Dictionary<string, string>
{
["path"] = obs.Path ?? "",
["process_id"] = obs.ProcessId.ToString()
["process_id"] = obs.ProcessId.ToString(CultureInfo.InvariantCulture)
}
});
}

View File

@@ -312,7 +312,7 @@ public static class CallGraphDigests
{
private static readonly JsonWriterOptions CanonicalJsonOptions = new()
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Default,
Indented = false,
SkipValidation = false
};

View File

@@ -0,0 +1,9 @@
# Scanner Contracts Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_apply.md` and `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| AUDIT-HOTLIST-SCANNER-CONTRACTS-0001 | DONE | Applied safe JSON encoder and test coverage update. |
| AUDIT-0946-A | DONE | Audit tracker updated for Scanner.Contracts apply. |

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>

View File

@@ -4,6 +4,8 @@ namespace StellaOps.Scanner.Surface.Secrets;
public interface ISurfaceSecretProvider
{
SurfaceSecretHandle Get(SurfaceSecretRequest request);
ValueTask<SurfaceSecretHandle> GetAsync(
SurfaceSecretRequest request,
CancellationToken cancellationToken = default);

View File

@@ -29,6 +29,50 @@ internal sealed class AuditingSurfaceSecretProvider : ISurfaceSecretProvider
_componentName = componentName ?? throw new ArgumentNullException(nameof(componentName));
}
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
{
var startTime = _timeProvider.GetUtcNow();
try
{
var handle = _inner.Get(request);
var elapsed = _timeProvider.GetUtcNow() - startTime;
LogAuditEvent(
request,
handle.Metadata,
success: true,
elapsed,
error: null);
return handle;
}
catch (SurfaceSecretNotFoundException)
{
var elapsed = _timeProvider.GetUtcNow() - startTime;
LogAuditEvent(
request,
metadata: null,
success: false,
elapsed,
error: "NotFound");
throw;
}
catch (Exception ex)
{
var elapsed = _timeProvider.GetUtcNow() - startTime;
LogAuditEvent(
request,
metadata: null,
success: false,
elapsed,
error: ex.GetType().Name);
throw;
}
}
public async ValueTask<SurfaceSecretHandle> GetAsync(
SurfaceSecretRequest request,
CancellationToken cancellationToken = default)

View File

@@ -35,6 +35,25 @@ internal sealed class CachingSurfaceSecretProvider : ISurfaceSecretProvider
public TimeSpan CacheTtl => _ttl;
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
{
var key = BuildCacheKey(request);
var now = _timeProvider.GetUtcNow();
if (_cache.TryGetValue(key, out var entry) && entry.ExpiresAt > now)
{
_logger.LogDebug("Surface secret cache hit for {Key}.", key);
return entry.Handle;
}
var handle = _inner.Get(request);
var newEntry = new CacheEntry(handle, now.Add(_ttl));
_cache[key] = newEntry;
_logger.LogDebug("Surface secret cached for {Key}, expires at {ExpiresAt}.", key, newEntry.ExpiresAt);
return handle;
}
public async ValueTask<SurfaceSecretHandle> GetAsync(
SurfaceSecretRequest request,
CancellationToken cancellationToken = default)

View File

@@ -18,6 +18,23 @@ internal sealed class CompositeSurfaceSecretProvider : ISurfaceSecretProvider
}
}
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
{
foreach (var provider in _providers)
{
try
{
return provider.Get(request);
}
catch (SurfaceSecretNotFoundException)
{
// try next provider
}
}
throw new SurfaceSecretNotFoundException(request);
}
public async ValueTask<SurfaceSecretHandle> GetAsync(
SurfaceSecretRequest request,
CancellationToken cancellationToken = default)

View File

@@ -20,6 +20,35 @@ internal sealed class FileSurfaceSecretProvider : ISurfaceSecretProvider
_root = root;
}
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
var path = ResolvePath(request);
if (!File.Exists(path))
{
throw new SurfaceSecretNotFoundException(request);
}
var json = File.ReadAllText(path);
var descriptor = JsonSerializer.Deserialize<FileSecretDescriptor>(json);
if (descriptor is null)
{
throw new SurfaceSecretNotFoundException(request);
}
if (string.IsNullOrWhiteSpace(descriptor.Payload))
{
return SurfaceSecretHandle.Empty;
}
var bytes = Convert.FromBase64String(descriptor.Payload);
return SurfaceSecretHandle.FromBytes(bytes, descriptor.Metadata);
}
public async ValueTask<SurfaceSecretHandle> GetAsync(
SurfaceSecretRequest request,
CancellationToken cancellationToken = default)

View File

@@ -21,6 +21,21 @@ public sealed class InMemorySurfaceSecretProvider : ISurfaceSecretProvider
_secrets[request.CacheKey] = handle;
}
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
if (_secrets.TryGetValue(request.CacheKey, out var handle))
{
return handle;
}
throw new SurfaceSecretNotFoundException(request);
}
public ValueTask<SurfaceSecretHandle> GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default)
{
if (request is null)

View File

@@ -14,6 +14,30 @@ internal sealed class InlineSurfaceSecretProvider : ISurfaceSecretProvider
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
{
if (!_configuration.AllowInline)
{
throw new SurfaceSecretNotFoundException(request);
}
var envKey = BuildEnvironmentKey(request);
var value = Environment.GetEnvironmentVariable(envKey);
if (string.IsNullOrWhiteSpace(value))
{
throw new SurfaceSecretNotFoundException(request);
}
var bytes = Convert.FromBase64String(value);
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["source"] = "inline-env",
["key"] = envKey
};
return SurfaceSecretHandle.FromBytes(bytes, metadata);
}
public ValueTask<SurfaceSecretHandle> GetAsync(
SurfaceSecretRequest request,
CancellationToken cancellationToken = default)

View File

@@ -23,6 +23,30 @@ internal sealed class KubernetesSurfaceSecretProvider : ISurfaceSecretProvider
}
}
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
{
var directory = Path.Combine(_configuration.Root!, request.Tenant, request.Component, request.SecretType);
if (!Directory.Exists(directory))
{
_logger.LogDebug("Kubernetes secret directory {Directory} not found.", directory);
throw new SurfaceSecretNotFoundException(request);
}
var name = request.Name ?? "default";
var payloadPath = Path.Combine(directory, name);
if (!File.Exists(payloadPath))
{
throw new SurfaceSecretNotFoundException(request);
}
var bytes = File.ReadAllBytes(payloadPath);
return SurfaceSecretHandle.FromBytes(bytes, new Dictionary<string, string>
{
["source"] = "kubernetes",
["path"] = payloadPath
});
}
public async ValueTask<SurfaceSecretHandle> GetAsync(
SurfaceSecretRequest request,
CancellationToken cancellationToken = default)

View File

@@ -42,6 +42,73 @@ internal sealed class OfflineSurfaceSecretProvider : ISurfaceSecretProvider
}
}
public SurfaceSecretHandle Get(SurfaceSecretRequest request)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
var directory = Path.Combine(_root, request.Tenant, request.Component, request.SecretType);
if (!Directory.Exists(directory))
{
_logger.LogDebug("Offline secret directory {Directory} not found.", directory);
throw new SurfaceSecretNotFoundException(request);
}
// Deterministic selection: if name is specified use it, otherwise pick lexicographically smallest
var targetName = request.Name ?? SelectDeterministicName(directory);
if (targetName is null)
{
throw new SurfaceSecretNotFoundException(request);
}
var path = Path.Combine(directory, targetName + ".json");
if (!File.Exists(path))
{
throw new SurfaceSecretNotFoundException(request);
}
var json = File.ReadAllText(path);
var descriptor = JsonSerializer.Deserialize<OfflineSecretDescriptor>(json);
if (descriptor is null)
{
throw new SurfaceSecretNotFoundException(request);
}
if (string.IsNullOrWhiteSpace(descriptor.Payload))
{
return SurfaceSecretHandle.Empty;
}
var bytes = Convert.FromBase64String(descriptor.Payload);
// Verify integrity if manifest entry exists
var manifestKey = BuildManifestKey(request.Tenant, request.Component, request.SecretType, targetName);
if (_manifest?.TryGetValue(manifestKey, out var entry) == true)
{
var actualHash = ComputeSha256(bytes);
if (!string.Equals(actualHash, entry.Sha256, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
"Offline secret integrity check failed for {Key}. Expected {Expected}, got {Actual}.",
manifestKey,
entry.Sha256,
actualHash);
throw new InvalidOperationException($"Offline secret integrity check failed for {manifestKey}.");
}
_logger.LogDebug("Offline secret integrity verified for {Key}.", manifestKey);
}
var metadata = descriptor.Metadata ?? new Dictionary<string, string>();
metadata["source"] = "offline";
metadata["path"] = path;
metadata["name"] = targetName;
return SurfaceSecretHandle.FromBytes(bytes, metadata);
}
public async ValueTask<SurfaceSecretHandle> GetAsync(
SurfaceSecretRequest request,
CancellationToken cancellationToken = default)