audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories

This commit is contained in:
master
2026-01-07 18:49:59 +02:00
parent 04ec098046
commit 608a7f85c0
866 changed files with 56323 additions and 6231 deletions

View File

@@ -0,0 +1,247 @@
// <copyright file="Spdx3ContextResolver.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Text.Json;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.Spdx3.JsonLd;
/// <summary>
/// Resolves JSON-LD contexts for SPDX 3.0.1 documents.
/// </summary>
public interface ISpdx3ContextResolver
{
/// <summary>
/// Resolves a context from a URL or embedded reference.
/// </summary>
/// <param name="contextRef">The context reference (URL or inline).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The resolved context as a JSON element.</returns>
Task<Spdx3Context?> ResolveAsync(
string contextRef,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Represents a resolved JSON-LD context.
/// </summary>
public sealed record Spdx3Context
{
/// <summary>
/// Gets the context URI.
/// </summary>
public required string Uri { get; init; }
/// <summary>
/// Gets the context document.
/// </summary>
public required JsonDocument Document { get; init; }
/// <summary>
/// Gets when the context was resolved.
/// </summary>
public required DateTimeOffset ResolvedAt { get; init; }
}
/// <summary>
/// Options for the SPDX 3 context resolver.
/// </summary>
public sealed class Spdx3ContextResolverOptions
{
/// <summary>
/// Gets or sets the cache TTL for resolved contexts.
/// </summary>
public TimeSpan CacheTtl { get; set; } = TimeSpan.FromHours(24);
/// <summary>
/// Gets or sets the maximum cache size.
/// </summary>
public int MaxCacheSize { get; set; } = 100;
/// <summary>
/// Gets or sets whether to allow remote context resolution.
/// Set to false for air-gapped environments.
/// </summary>
public bool AllowRemoteContexts { get; set; } = true;
/// <summary>
/// Gets or sets the base path for local context files.
/// </summary>
public string? LocalContextPath { get; set; }
/// <summary>
/// Gets or sets the HTTP timeout for remote contexts.
/// </summary>
public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(30);
}
/// <summary>
/// Implementation of context resolver with caching.
/// </summary>
public sealed class Spdx3ContextResolver : ISpdx3ContextResolver, IDisposable
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IMemoryCache _cache;
private readonly ILogger<Spdx3ContextResolver> _logger;
private readonly Spdx3ContextResolverOptions _options;
private readonly TimeProvider _timeProvider;
private static readonly Dictionary<string, string> EmbeddedContexts = new(StringComparer.OrdinalIgnoreCase)
{
["https://spdx.org/rdf/3.0.1/spdx-context.jsonld"] = GetEmbeddedContext("spdx-context.jsonld"),
["https://spdx.org/rdf/3.0.1/terms/Core"] = GetEmbeddedContext("core-profile.jsonld"),
["https://spdx.org/rdf/3.0.1/terms/Software"] = GetEmbeddedContext("software-profile.jsonld"),
["https://spdx.org/rdf/3.0.1/terms/Security"] = GetEmbeddedContext("security-profile.jsonld"),
["https://spdx.org/rdf/3.0.1/terms/Build"] = GetEmbeddedContext("build-profile.jsonld"),
["https://spdx.org/rdf/3.0.1/terms/Lite"] = GetEmbeddedContext("lite-profile.jsonld")
};
/// <summary>
/// Initializes a new instance of the <see cref="Spdx3ContextResolver"/> class.
/// </summary>
public Spdx3ContextResolver(
IHttpClientFactory httpClientFactory,
IMemoryCache cache,
ILogger<Spdx3ContextResolver> logger,
IOptions<Spdx3ContextResolverOptions> options,
TimeProvider timeProvider)
{
_httpClientFactory = httpClientFactory;
_cache = cache;
_logger = logger;
_options = options.Value;
_timeProvider = timeProvider;
}
/// <inheritdoc />
public async Task<Spdx3Context?> ResolveAsync(
string contextRef,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(contextRef))
{
return null;
}
// Check cache first
var cacheKey = $"spdx3-context:{contextRef}";
if (_cache.TryGetValue(cacheKey, out Spdx3Context? cached))
{
_logger.LogDebug("Context cache hit for {ContextRef}", contextRef);
return cached;
}
// Try embedded contexts first (for air-gap support)
if (EmbeddedContexts.TryGetValue(contextRef, out var embedded))
{
var context = CreateContext(contextRef, embedded);
CacheContext(cacheKey, context);
return context;
}
// Try local file if configured
if (!string.IsNullOrEmpty(_options.LocalContextPath))
{
var localPath = Path.Combine(_options.LocalContextPath, GetContextFileName(contextRef));
if (File.Exists(localPath))
{
var content = await File.ReadAllTextAsync(localPath, cancellationToken)
.ConfigureAwait(false);
var context = CreateContext(contextRef, content);
CacheContext(cacheKey, context);
return context;
}
}
// Fetch remote if allowed
if (!_options.AllowRemoteContexts)
{
_logger.LogWarning("Remote context resolution disabled, cannot resolve {ContextRef}", contextRef);
return null;
}
return await FetchRemoteContextAsync(contextRef, cacheKey, cancellationToken)
.ConfigureAwait(false);
}
private async Task<Spdx3Context?> FetchRemoteContextAsync(
string contextRef,
string cacheKey,
CancellationToken cancellationToken)
{
try
{
var client = _httpClientFactory.CreateClient("Spdx3Context");
client.Timeout = _options.HttpTimeout;
_logger.LogInformation("Fetching remote context {ContextRef}", contextRef);
var content = await client.GetStringAsync(contextRef, cancellationToken)
.ConfigureAwait(false);
var context = CreateContext(contextRef, content);
CacheContext(cacheKey, context);
return context;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch remote context {ContextRef}", contextRef);
return null;
}
}
private Spdx3Context CreateContext(string uri, string content)
{
return new Spdx3Context
{
Uri = uri,
Document = JsonDocument.Parse(content),
ResolvedAt = _timeProvider.GetUtcNow()
};
}
private void CacheContext(string cacheKey, Spdx3Context context)
{
var cacheOptions = new MemoryCacheEntryOptions
{
Size = 1,
SlidingExpiration = _options.CacheTtl
};
_cache.Set(cacheKey, context, cacheOptions);
}
private static string GetContextFileName(string uri)
{
var lastSlash = uri.LastIndexOf('/');
return lastSlash >= 0 ? uri[(lastSlash + 1)..] : uri;
}
private static string GetEmbeddedContext(string name)
{
// In a real implementation, this would load from embedded resources
// For now, return a minimal stub that allows parsing
return name switch
{
"spdx-context.jsonld" => """
{
"@context": {
"spdx": "https://spdx.org/rdf/3.0.1/terms/",
"Core": "spdx:Core/",
"Software": "spdx:Software/",
"spdxId": "@id",
"@type": "@type"
}
}
""",
_ => "{}"
};
}
/// <inheritdoc />
public void Dispose()
{
// IMemoryCache is typically managed by DI
}
}