Files
git.stella-ops.org/src/Policy/__Libraries/StellaOps.Policy/Replay/KnowledgeSourceResolver.cs
StellaOps Bot 5146204f1b feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
2025-12-22 23:21:21 +02:00

196 lines
5.6 KiB
C#

using System.Security.Cryptography;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Policy.Snapshots;
namespace StellaOps.Policy.Replay;
/// <summary>
/// Resolves knowledge sources from snapshot descriptors.
/// </summary>
public sealed class KnowledgeSourceResolver : IKnowledgeSourceResolver
{
private readonly ISnapshotStore _snapshotStore;
private readonly ILogger<KnowledgeSourceResolver> _logger;
public KnowledgeSourceResolver(
ISnapshotStore snapshotStore,
ILogger<KnowledgeSourceResolver>? logger = null)
{
_snapshotStore = snapshotStore ?? throw new ArgumentNullException(nameof(snapshotStore));
_logger = logger ?? NullLogger<KnowledgeSourceResolver>.Instance;
}
/// <summary>
/// Resolves a knowledge source to its actual content.
/// </summary>
public async Task<ResolvedSource?> ResolveAsync(
KnowledgeSourceDescriptor descriptor,
bool allowNetworkFetch,
CancellationToken ct = default)
{
_logger.LogDebug("Resolving source {Name} ({Type})", descriptor.Name, descriptor.Type);
// Try bundled content first
if (descriptor.InclusionMode != SourceInclusionMode.Referenced &&
descriptor.BundlePath is not null)
{
var bundled = await ResolveBundledAsync(descriptor, ct).ConfigureAwait(false);
if (bundled is not null)
return bundled;
}
// Try local store by digest
var local = await ResolveFromLocalStoreAsync(descriptor, ct).ConfigureAwait(false);
if (local is not null)
return local;
// Network fetch not implemented yet (air-gap safe default)
if (allowNetworkFetch && descriptor.Origin is not null)
{
_logger.LogWarning("Network fetch not implemented for {Name}", descriptor.Name);
}
_logger.LogWarning("Failed to resolve source {Name} with digest {Digest}",
descriptor.Name, descriptor.Digest);
return null;
}
private async Task<ResolvedSource?> ResolveBundledAsync(
KnowledgeSourceDescriptor descriptor,
CancellationToken ct)
{
try
{
var content = await _snapshotStore.GetBundledContentAsync(descriptor.BundlePath!, ct)
.ConfigureAwait(false);
if (content is null)
return null;
// Verify digest
var actualDigest = ComputeDigest(content);
if (actualDigest != descriptor.Digest)
{
_logger.LogWarning(
"Bundled source {Name} digest mismatch: expected {Expected}, got {Actual}",
descriptor.Name, descriptor.Digest, actualDigest);
return null;
}
return new ResolvedSource(
descriptor.Name,
descriptor.Type,
content,
SourceResolutionMethod.Bundled);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to resolve bundled source {Name}", descriptor.Name);
return null;
}
}
private async Task<ResolvedSource?> ResolveFromLocalStoreAsync(
KnowledgeSourceDescriptor descriptor,
CancellationToken ct)
{
try
{
var content = await _snapshotStore.GetByDigestAsync(descriptor.Digest, ct)
.ConfigureAwait(false);
if (content is null)
return null;
return new ResolvedSource(
descriptor.Name,
descriptor.Type,
content,
SourceResolutionMethod.LocalStore);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to resolve source {Name} from local store", descriptor.Name);
return null;
}
}
private static string ComputeDigest(byte[] content)
{
var hash = SHA256.HashData(content);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}
/// <summary>
/// Resolved knowledge source with content.
/// </summary>
public sealed record ResolvedSource(
string Name,
string Type,
byte[] Content,
SourceResolutionMethod Method);
/// <summary>
/// Method used to resolve a source.
/// </summary>
public enum SourceResolutionMethod
{
Bundled,
LocalStore,
NetworkFetch
}
/// <summary>
/// Interface for source resolution.
/// </summary>
public interface IKnowledgeSourceResolver
{
Task<ResolvedSource?> ResolveAsync(
KnowledgeSourceDescriptor descriptor,
bool allowNetworkFetch,
CancellationToken ct = default);
}
/// <summary>
/// Frozen inputs for replay.
/// </summary>
public sealed class FrozenInputs
{
public Dictionary<string, ResolvedSource> ResolvedSources { get; } = new();
public IReadOnlyList<string> MissingSources { get; init; } = [];
public bool IsComplete => MissingSources.Count == 0;
}
/// <summary>
/// Builder for frozen inputs.
/// </summary>
public sealed class FrozenInputsBuilder
{
private readonly Dictionary<string, ResolvedSource> _sources = new();
public FrozenInputsBuilder AddSource(string name, ResolvedSource source)
{
_sources[name] = source;
return this;
}
public FrozenInputs Build(IReadOnlyList<string> missingSources)
{
var inputs = new FrozenInputs
{
MissingSources = missingSources
};
// Copy resolved sources
foreach (var kvp in _sources)
{
inputs.ResolvedSources[kvp.Key] = kvp.Value;
}
return inputs;
}
}