- 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.
196 lines
5.6 KiB
C#
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;
|
|
}
|
|
}
|