up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
Vladimir Moushkov
2025-10-24 19:19:23 +03:00
parent 17d861e4ab
commit b51037a9b8
72 changed files with 6070 additions and 151 deletions

View File

@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Zastava.Core.Contracts;
using StellaOps.Zastava.Observer.Backend;
using StellaOps.Zastava.Observer.Configuration;
using StellaOps.Zastava.Observer.ContainerRuntime.Cri;
using StellaOps.Zastava.Observer.Posture;
using Xunit;
namespace StellaOps.Zastava.Observer.Tests.Posture;
public sealed class RuntimePostureEvaluatorTests
{
[Fact]
public async Task EvaluateAsync_BacksOffToBackendAndCachesEntry()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 24, 12, 0, 0, TimeSpan.Zero));
var cache = new StubPostureCache();
var options = CreateOptions();
var client = new StubPolicyClient(image =>
{
var result = new RuntimePolicyImageResult
{
Signed = true,
HasSbomReferrers = true,
Rekor = new RuntimePolicyRekorResult
{
Uuid = "rekor-123",
Verified = true
}
};
return new RuntimePolicyResponse
{
TtlSeconds = 600,
ExpiresAtUtc = timeProvider.GetUtcNow().AddMinutes(10),
Results = new Dictionary<string, RuntimePolicyImageResult>(StringComparer.Ordinal)
{
[image] = result
}
};
});
var evaluator = new RuntimePostureEvaluator(client, cache, options, timeProvider, NullLogger<RuntimePostureEvaluator>.Instance);
var container = CreateContainerInfo();
var evaluation = await evaluator.EvaluateAsync(container, CancellationToken.None);
Assert.NotNull(evaluation.Posture);
Assert.True(evaluation.Posture!.ImageSigned);
Assert.Equal("present", evaluation.Posture.SbomReferrer);
Assert.Contains(evaluation.Evidence, e => e.Signal == "runtime.posture.source" && e.Value == "backend");
var cached = cache.Get(container.ImageRef!);
Assert.NotNull(cached);
}
[Fact]
public async Task EvaluateAsync_UsesCacheWhenBackendFails()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 24, 12, 0, 0, TimeSpan.Zero));
var cache = new StubPostureCache();
var options = CreateOptions();
var imageRef = "ghcr.io/example/app@sha256:deadbeef";
var cachedPosture = new RuntimePosture
{
ImageSigned = false,
SbomReferrer = "missing"
};
cache.Seed(imageRef, cachedPosture, timeProvider.GetUtcNow().AddMinutes(-1), timeProvider.GetUtcNow().AddMinutes(-10));
var client = new StubPolicyClient(_ => throw new InvalidOperationException("backend unavailable"));
var evaluator = new RuntimePostureEvaluator(client, cache, options, timeProvider, NullLogger<RuntimePostureEvaluator>.Instance);
var container = CreateContainerInfo(imageRef);
var evaluation = await evaluator.EvaluateAsync(container, CancellationToken.None);
Assert.NotNull(evaluation.Posture);
Assert.False(evaluation.Posture!.ImageSigned);
Assert.Contains(evaluation.Evidence, e => e.Signal == "runtime.posture.cache");
Assert.Contains(evaluation.Evidence, e => e.Signal == "runtime.posture.error");
}
private static CriContainerInfo CreateContainerInfo(string? imageRef = null)
{
var labels = new Dictionary<string, string>(StringComparer.Ordinal)
{
[CriLabelKeys.PodNamespace] = "payments",
[CriLabelKeys.PodName] = "api-pod",
[CriLabelKeys.ContainerName] = "api"
};
return new CriContainerInfo(
Id: "container-a",
PodSandboxId: "sandbox-a",
Name: "api",
Attempt: 1,
Image: "ghcr.io/example/app:1.0.0",
ImageRef: imageRef ?? "ghcr.io/example/app@sha256:deadbeef",
Labels: labels,
Annotations: new Dictionary<string, string>(StringComparer.Ordinal),
CreatedAt: DateTimeOffset.UtcNow,
StartedAt: DateTimeOffset.UtcNow,
FinishedAt: null,
ExitCode: null,
Reason: null,
Message: null,
Pid: 1234);
}
private static TestOptionsMonitor<ZastavaObserverOptions> CreateOptions()
{
var options = new ZastavaObserverOptions
{
Posture = new ZastavaObserverPostureOptions
{
CachePath = Path.Combine(Path.GetTempPath(), "zastava-observer-tests", Guid.NewGuid().ToString("N"), "posture-cache.json"),
FallbackTtlSeconds = 300,
StaleWarningThresholdSeconds = 600
}
};
return new TestOptionsMonitor<ZastavaObserverOptions>(options);
}
private sealed class StubPolicyClient : IRuntimePolicyClient
{
private readonly Func<string, RuntimePolicyResponse> factory;
public StubPolicyClient(Func<string, RuntimePolicyResponse> factory)
{
this.factory = factory;
}
public Task<RuntimePolicyResponse> EvaluateAsync(RuntimePolicyRequest request, CancellationToken cancellationToken = default)
{
var image = request.Images.First();
return Task.FromResult(factory(image));
}
}
private sealed class StubPostureCache : IRuntimePostureCache
{
private readonly Dictionary<string, RuntimePostureCacheEntry> entries = new(StringComparer.Ordinal);
public RuntimePostureCacheEntry? Get(string key)
{
entries.TryGetValue(key, out var entry);
return entry;
}
public void Seed(string key, RuntimePosture posture, DateTimeOffset expiresAt, DateTimeOffset storedAt)
{
entries[key] = new RuntimePostureCacheEntry(posture, expiresAt, storedAt);
}
public void Set(string key, RuntimePosture posture, DateTimeOffset expiresAtUtc, DateTimeOffset storedAtUtc)
{
entries[key] = new RuntimePostureCacheEntry(posture, expiresAtUtc, storedAtUtc);
}
}
private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
{
private readonly T value;
public TestOptionsMonitor(T value)
{
this.value = value;
}
public T CurrentValue => value;
public T Get(string? name) => value;
public IDisposable OnChange(Action<T, string?> listener) => NullDisposable.Instance;
private sealed class NullDisposable : IDisposable
{
public static readonly NullDisposable Instance = new();
public void Dispose()
{
}
}
}
}