Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
- Added ConsoleExportClient for managing export requests and responses. - Introduced ConsoleExportRequest and ConsoleExportResponse models. - Implemented methods for creating and retrieving exports with appropriate headers. feat(crypto): Add Software SM2/SM3 Cryptography Provider - Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography. - Added support for signing and verification using SM2 algorithm. - Included hashing functionality with SM3 algorithm. - Configured options for loading keys from files and environment gate checks. test(crypto): Add unit tests for SmSoftCryptoProvider - Created comprehensive tests for signing, verifying, and hashing functionalities. - Ensured correct behavior for key management and error handling. feat(api): Enhance Console Export Models - Expanded ConsoleExport models to include detailed status and event types. - Added support for various export formats and notification options. test(time): Implement TimeAnchorPolicyService tests - Developed tests for TimeAnchorPolicyService to validate time anchors. - Covered scenarios for anchor validation, drift calculation, and policy enforcement.
262 lines
9.1 KiB
C#
262 lines
9.1 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.AirGap.Time.Models;
|
|
using StellaOps.AirGap.Time.Services;
|
|
using StellaOps.AirGap.Time.Stores;
|
|
|
|
namespace StellaOps.AirGap.Time.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for TimeAnchorPolicyService.
|
|
/// Per AIRGAP-TIME-57-001: Time-anchor policy enforcement.
|
|
/// </summary>
|
|
public class TimeAnchorPolicyServiceTests
|
|
{
|
|
private readonly TimeProvider _fixedTimeProvider;
|
|
private readonly InMemoryTimeAnchorStore _store;
|
|
private readonly StalenessCalculator _calculator;
|
|
private readonly TimeTelemetry _telemetry;
|
|
private readonly TimeStatusService _statusService;
|
|
private readonly AirGapOptions _airGapOptions;
|
|
|
|
public TimeAnchorPolicyServiceTests()
|
|
{
|
|
_fixedTimeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 15, 12, 0, 0, TimeSpan.Zero));
|
|
_store = new InMemoryTimeAnchorStore();
|
|
_calculator = new StalenessCalculator();
|
|
_telemetry = new TimeTelemetry();
|
|
_airGapOptions = new AirGapOptions
|
|
{
|
|
Staleness = new AirGapOptions.StalenessOptions { WarningSeconds = 3600, BreachSeconds = 7200 },
|
|
ContentBudgets = new Dictionary<string, AirGapOptions.StalenessOptions>()
|
|
};
|
|
_statusService = new TimeStatusService(_store, _calculator, _telemetry, Options.Create(_airGapOptions));
|
|
}
|
|
|
|
private TimeAnchorPolicyService CreateService(TimeAnchorPolicyOptions? options = null)
|
|
{
|
|
return new TimeAnchorPolicyService(
|
|
_statusService,
|
|
Options.Create(options ?? new TimeAnchorPolicyOptions()),
|
|
NullLogger<TimeAnchorPolicyService>.Instance,
|
|
_fixedTimeProvider);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateTimeAnchorAsync_ReturnsFailure_WhenNoAnchor()
|
|
{
|
|
var service = CreateService();
|
|
|
|
var result = await service.ValidateTimeAnchorAsync("tenant-1");
|
|
|
|
Assert.False(result.Allowed);
|
|
Assert.Equal(TimeAnchorPolicyErrorCodes.AnchorMissing, result.ErrorCode);
|
|
Assert.NotNull(result.Remediation);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateTimeAnchorAsync_ReturnsSuccess_WhenAnchorValid()
|
|
{
|
|
var service = CreateService();
|
|
var anchor = new TimeAnchor(
|
|
_fixedTimeProvider.GetUtcNow().AddMinutes(-30),
|
|
"test-source",
|
|
"Roughtime",
|
|
"fingerprint",
|
|
"digest123");
|
|
var budget = new StalenessBudget(3600, 7200);
|
|
|
|
await _store.SetAsync("tenant-1", anchor, budget, CancellationToken.None);
|
|
|
|
var result = await service.ValidateTimeAnchorAsync("tenant-1");
|
|
|
|
Assert.True(result.Allowed);
|
|
Assert.Null(result.ErrorCode);
|
|
Assert.NotNull(result.Staleness);
|
|
Assert.False(result.Staleness.IsBreach);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateTimeAnchorAsync_ReturnsWarning_WhenAnchorStale()
|
|
{
|
|
var service = CreateService();
|
|
var anchor = new TimeAnchor(
|
|
_fixedTimeProvider.GetUtcNow().AddSeconds(-5000), // Past warning threshold
|
|
"test-source",
|
|
"Roughtime",
|
|
"fingerprint",
|
|
"digest123");
|
|
var budget = new StalenessBudget(3600, 7200);
|
|
|
|
await _store.SetAsync("tenant-1", anchor, budget, CancellationToken.None);
|
|
|
|
var result = await service.ValidateTimeAnchorAsync("tenant-1");
|
|
|
|
Assert.True(result.Allowed); // Allowed but with warning
|
|
Assert.NotNull(result.Staleness);
|
|
Assert.True(result.Staleness.IsWarning);
|
|
Assert.Contains("warning", result.Reason, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ValidateTimeAnchorAsync_ReturnsFailure_WhenAnchorBreached()
|
|
{
|
|
var service = CreateService();
|
|
var anchor = new TimeAnchor(
|
|
_fixedTimeProvider.GetUtcNow().AddSeconds(-8000), // Past breach threshold
|
|
"test-source",
|
|
"Roughtime",
|
|
"fingerprint",
|
|
"digest123");
|
|
var budget = new StalenessBudget(3600, 7200);
|
|
|
|
await _store.SetAsync("tenant-1", anchor, budget, CancellationToken.None);
|
|
|
|
var result = await service.ValidateTimeAnchorAsync("tenant-1");
|
|
|
|
Assert.False(result.Allowed);
|
|
Assert.Equal(TimeAnchorPolicyErrorCodes.AnchorBreached, result.ErrorCode);
|
|
Assert.NotNull(result.Staleness);
|
|
Assert.True(result.Staleness.IsBreach);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnforceBundleImportPolicyAsync_AllowsImport_WhenAnchorValid()
|
|
{
|
|
var service = CreateService();
|
|
var anchor = new TimeAnchor(
|
|
_fixedTimeProvider.GetUtcNow().AddMinutes(-30),
|
|
"test-source",
|
|
"Roughtime",
|
|
"fingerprint",
|
|
"digest123");
|
|
var budget = new StalenessBudget(3600, 7200);
|
|
|
|
await _store.SetAsync("tenant-1", anchor, budget, CancellationToken.None);
|
|
|
|
var result = await service.EnforceBundleImportPolicyAsync(
|
|
"tenant-1",
|
|
"bundle-123",
|
|
_fixedTimeProvider.GetUtcNow().AddMinutes(-15));
|
|
|
|
Assert.True(result.Allowed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnforceBundleImportPolicyAsync_BlocksImport_WhenDriftExceeded()
|
|
{
|
|
var options = new TimeAnchorPolicyOptions { MaxDriftSeconds = 3600 }; // 1 hour max
|
|
var service = CreateService(options);
|
|
var anchor = new TimeAnchor(
|
|
_fixedTimeProvider.GetUtcNow().AddMinutes(-30),
|
|
"test-source",
|
|
"Roughtime",
|
|
"fingerprint",
|
|
"digest123");
|
|
var budget = new StalenessBudget(86400, 172800); // Large budget
|
|
|
|
await _store.SetAsync("tenant-1", anchor, budget, CancellationToken.None);
|
|
|
|
var bundleTimestamp = _fixedTimeProvider.GetUtcNow().AddDays(-2); // 2 days ago
|
|
|
|
var result = await service.EnforceBundleImportPolicyAsync(
|
|
"tenant-1",
|
|
"bundle-123",
|
|
bundleTimestamp);
|
|
|
|
Assert.False(result.Allowed);
|
|
Assert.Equal(TimeAnchorPolicyErrorCodes.DriftExceeded, result.ErrorCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnforceOperationPolicyAsync_BlocksStrictOperations_WhenNoAnchor()
|
|
{
|
|
var options = new TimeAnchorPolicyOptions
|
|
{
|
|
StrictOperations = new[] { "attestation.sign" }
|
|
};
|
|
var service = CreateService(options);
|
|
|
|
var result = await service.EnforceOperationPolicyAsync("tenant-1", "attestation.sign");
|
|
|
|
Assert.False(result.Allowed);
|
|
Assert.Equal(TimeAnchorPolicyErrorCodes.AnchorMissing, result.ErrorCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnforceOperationPolicyAsync_AllowsNonStrictOperations_InNonStrictMode()
|
|
{
|
|
var options = new TimeAnchorPolicyOptions
|
|
{
|
|
StrictEnforcement = false,
|
|
StrictOperations = new[] { "attestation.sign" }
|
|
};
|
|
var service = CreateService(options);
|
|
|
|
var result = await service.EnforceOperationPolicyAsync("tenant-1", "some.other.operation");
|
|
|
|
Assert.True(result.Allowed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CalculateDriftAsync_ReturnsNoDrift_WhenNoAnchor()
|
|
{
|
|
var service = CreateService();
|
|
|
|
var result = await service.CalculateDriftAsync("tenant-1", _fixedTimeProvider.GetUtcNow());
|
|
|
|
Assert.False(result.HasAnchor);
|
|
Assert.Equal(TimeSpan.Zero, result.Drift);
|
|
Assert.Null(result.AnchorTime);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CalculateDriftAsync_ReturnsDrift_WhenAnchorExists()
|
|
{
|
|
var service = CreateService(new TimeAnchorPolicyOptions { MaxDriftSeconds = 3600 });
|
|
var anchorTime = _fixedTimeProvider.GetUtcNow().AddMinutes(-30);
|
|
var anchor = new TimeAnchor(anchorTime, "test", "Roughtime", "fp", "digest");
|
|
var budget = new StalenessBudget(3600, 7200);
|
|
|
|
await _store.SetAsync("tenant-1", anchor, budget, CancellationToken.None);
|
|
|
|
var targetTime = _fixedTimeProvider.GetUtcNow().AddMinutes(15);
|
|
var result = await service.CalculateDriftAsync("tenant-1", targetTime);
|
|
|
|
Assert.True(result.HasAnchor);
|
|
Assert.Equal(anchorTime, result.AnchorTime);
|
|
Assert.Equal(45, (int)result.Drift.TotalMinutes); // 30 min + 15 min
|
|
Assert.False(result.DriftExceedsThreshold);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CalculateDriftAsync_DetectsExcessiveDrift()
|
|
{
|
|
var service = CreateService(new TimeAnchorPolicyOptions { MaxDriftSeconds = 60 }); // 1 minute max
|
|
var anchor = new TimeAnchor(
|
|
_fixedTimeProvider.GetUtcNow(),
|
|
"test",
|
|
"Roughtime",
|
|
"fp",
|
|
"digest");
|
|
var budget = new StalenessBudget(3600, 7200);
|
|
|
|
await _store.SetAsync("tenant-1", anchor, budget, CancellationToken.None);
|
|
|
|
var targetTime = _fixedTimeProvider.GetUtcNow().AddMinutes(5); // 5 minutes drift
|
|
var result = await service.CalculateDriftAsync("tenant-1", targetTime);
|
|
|
|
Assert.True(result.HasAnchor);
|
|
Assert.True(result.DriftExceedsThreshold);
|
|
}
|
|
|
|
private sealed class FakeTimeProvider : TimeProvider
|
|
{
|
|
private readonly DateTimeOffset _now;
|
|
|
|
public FakeTimeProvider(DateTimeOffset now) => _now = now;
|
|
|
|
public override DateTimeOffset GetUtcNow() => _now;
|
|
}
|
|
}
|