Add call graph fixtures for various languages and scenarios
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (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
Lighthouse CI / Lighthouse Audit (push) Has been cancelled
Lighthouse CI / Axe Accessibility Audit (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Reachability Corpus Validation / validate-corpus (push) Has been cancelled
Reachability Corpus Validation / validate-ground-truths (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Reachability Corpus Validation / determinism-check (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

- Introduced `all-edge-reasons.json` to test edge resolution reasons in .NET.
- Added `all-visibility-levels.json` to validate method visibility levels in .NET.
- Created `dotnet-aspnetcore-minimal.json` for a minimal ASP.NET Core application.
- Included `go-gin-api.json` for a Go Gin API application structure.
- Added `java-spring-boot.json` for the Spring PetClinic application in Java.
- Introduced `legacy-no-schema.json` for legacy application structure without schema.
- Created `node-express-api.json` for an Express.js API application structure.
This commit is contained in:
master
2025-12-16 10:44:24 +02:00
parent 4391f35d8a
commit 5a480a3c2a
223 changed files with 19367 additions and 727 deletions

View File

@@ -0,0 +1,29 @@
-- Authority Schema Migration 004: Offline Kit Audit
-- Sprint: SPRINT_0341_0001_0001 - Observability & Audit Enhancements
-- Purpose: Store structured Offline Kit import/activation audit events per advisory §13.2.
CREATE TABLE IF NOT EXISTS authority.offline_kit_audit (
event_id UUID PRIMARY KEY,
tenant_id TEXT NOT NULL,
event_type TEXT NOT NULL,
timestamp TIMESTAMPTZ NOT NULL,
actor TEXT NOT NULL,
details JSONB NOT NULL,
result TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_ts ON authority.offline_kit_audit(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_type ON authority.offline_kit_audit(event_type);
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_tenant_ts ON authority.offline_kit_audit(tenant_id, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_offline_kit_audit_result ON authority.offline_kit_audit(tenant_id, result, timestamp DESC);
-- RLS (authority_app.require_current_tenant was introduced in migration 003_enable_rls.sql)
ALTER TABLE authority.offline_kit_audit ENABLE ROW LEVEL SECURITY;
ALTER TABLE authority.offline_kit_audit FORCE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS offline_kit_audit_tenant_isolation ON authority.offline_kit_audit;
CREATE POLICY offline_kit_audit_tenant_isolation ON authority.offline_kit_audit
FOR ALL
USING (tenant_id = authority_app.require_current_tenant())
WITH CHECK (tenant_id = authority_app.require_current_tenant());

View File

@@ -0,0 +1,16 @@
namespace StellaOps.Authority.Storage.Postgres.Models;
/// <summary>
/// Represents an Offline Kit audit record.
/// </summary>
public sealed class OfflineKitAuditEntity
{
public required Guid EventId { get; init; }
public required string TenantId { get; init; }
public required string EventType { get; init; }
public DateTimeOffset Timestamp { get; init; }
public required string Actor { get; init; }
public required string Details { get; init; }
public required string Result { get; init; }
}

View File

@@ -0,0 +1,9 @@
using StellaOps.Authority.Storage.Postgres.Models;
namespace StellaOps.Authority.Storage.Postgres.Repositories;
public interface IOfflineKitAuditEmitter
{
Task RecordAsync(OfflineKitAuditEntity entity, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,17 @@
using StellaOps.Authority.Storage.Postgres.Models;
namespace StellaOps.Authority.Storage.Postgres.Repositories;
public interface IOfflineKitAuditRepository
{
Task InsertAsync(OfflineKitAuditEntity entity, CancellationToken cancellationToken = default);
Task<IReadOnlyList<OfflineKitAuditEntity>> ListAsync(
string tenantId,
string? eventType = null,
string? result = null,
int limit = 100,
int offset = 0,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,40 @@
using Microsoft.Extensions.Logging;
using StellaOps.Authority.Storage.Postgres.Models;
namespace StellaOps.Authority.Storage.Postgres.Repositories;
/// <summary>
/// Emits Offline Kit audit records to PostgreSQL.
/// Audit failures should not break import flows.
/// </summary>
public sealed class OfflineKitAuditEmitter : IOfflineKitAuditEmitter
{
private readonly IOfflineKitAuditRepository _repository;
private readonly ILogger<OfflineKitAuditEmitter> _logger;
public OfflineKitAuditEmitter(IOfflineKitAuditRepository repository, ILogger<OfflineKitAuditEmitter> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task RecordAsync(OfflineKitAuditEntity entity, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(entity);
try
{
await _repository.InsertAsync(entity, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(
ex,
"offlinekit.audit.record failed tenant_id={tenant_id} event_type={event_type} event_id={event_id}",
entity.TenantId,
entity.EventType,
entity.EventId);
}
}
}

View File

@@ -0,0 +1,103 @@
using Microsoft.Extensions.Logging;
using Npgsql;
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Infrastructure.Postgres.Repositories;
namespace StellaOps.Authority.Storage.Postgres.Repositories;
/// <summary>
/// PostgreSQL repository for Offline Kit audit records.
/// </summary>
public sealed class OfflineKitAuditRepository : RepositoryBase<AuthorityDataSource>, IOfflineKitAuditRepository
{
public OfflineKitAuditRepository(AuthorityDataSource dataSource, ILogger<OfflineKitAuditRepository> logger)
: base(dataSource, logger)
{
}
public async Task InsertAsync(OfflineKitAuditEntity entity, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(entity);
ArgumentException.ThrowIfNullOrWhiteSpace(entity.TenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(entity.EventType);
ArgumentException.ThrowIfNullOrWhiteSpace(entity.Actor);
ArgumentException.ThrowIfNullOrWhiteSpace(entity.Details);
ArgumentException.ThrowIfNullOrWhiteSpace(entity.Result);
const string sql = """
INSERT INTO authority.offline_kit_audit
(event_id, tenant_id, event_type, timestamp, actor, details, result)
VALUES (@event_id, @tenant_id, @event_type, @timestamp, @actor, @details::jsonb, @result)
""";
await ExecuteAsync(
tenantId: entity.TenantId,
sql: sql,
configureCommand: cmd =>
{
AddParameter(cmd, "event_id", entity.EventId);
AddParameter(cmd, "tenant_id", entity.TenantId);
AddParameter(cmd, "event_type", entity.EventType);
AddParameter(cmd, "timestamp", entity.Timestamp);
AddParameter(cmd, "actor", entity.Actor);
AddJsonbParameter(cmd, "details", entity.Details);
AddParameter(cmd, "result", entity.Result);
},
cancellationToken: cancellationToken).ConfigureAwait(false);
}
public async Task<IReadOnlyList<OfflineKitAuditEntity>> ListAsync(
string tenantId,
string? eventType = null,
string? result = null,
int limit = 100,
int offset = 0,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
limit = Math.Clamp(limit, 1, 1000);
offset = Math.Max(0, offset);
var (whereClause, whereParameters) = BuildWhereClause(
("tenant_id = @tenant_id", "tenant_id", tenantId, include: true),
("event_type = @event_type", "event_type", eventType, include: !string.IsNullOrWhiteSpace(eventType)),
("result = @result", "result", result, include: !string.IsNullOrWhiteSpace(result)));
var sql = $"""
SELECT event_id, tenant_id, event_type, timestamp, actor, details, result
FROM authority.offline_kit_audit
{whereClause}
ORDER BY timestamp DESC, event_id DESC
LIMIT @limit OFFSET @offset
""";
return await QueryAsync(
tenantId: tenantId,
sql: sql,
configureCommand: cmd =>
{
foreach (var (name, value) in whereParameters)
{
AddParameter(cmd, name, value);
}
AddParameter(cmd, "limit", limit);
AddParameter(cmd, "offset", offset);
},
mapRow: MapAudit,
cancellationToken: cancellationToken).ConfigureAwait(false);
}
private static OfflineKitAuditEntity MapAudit(NpgsqlDataReader reader) => new()
{
EventId = reader.GetGuid(0),
TenantId = reader.GetString(1),
EventType = reader.GetString(2),
Timestamp = reader.GetFieldValue<DateTimeOffset>(3),
Actor = reader.GetString(4),
Details = reader.GetString(5),
Result = reader.GetString(6)
};
}

View File

@@ -75,6 +75,9 @@ public static class ServiceCollectionExtensions
services.AddScoped<LoginAttemptRepository>();
services.AddScoped<OidcTokenRepository>();
services.AddScoped<AirgapAuditRepository>();
services.AddScoped<OfflineKitAuditRepository>();
services.AddScoped<IOfflineKitAuditRepository>(sp => sp.GetRequiredService<OfflineKitAuditRepository>());
services.AddScoped<IOfflineKitAuditEmitter, OfflineKitAuditEmitter>();
services.AddScoped<RevocationExportStateRepository>();
}
}

View File

@@ -0,0 +1,127 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Authority.Storage.Postgres.Models;
using StellaOps.Authority.Storage.Postgres.Repositories;
using Xunit;
namespace StellaOps.Authority.Storage.Postgres.Tests;
[Collection(AuthorityPostgresCollection.Name)]
public sealed class OfflineKitAuditRepositoryTests : IAsyncLifetime
{
private readonly AuthorityPostgresFixture _fixture;
private readonly OfflineKitAuditRepository _repository;
public OfflineKitAuditRepositoryTests(AuthorityPostgresFixture fixture)
{
_fixture = fixture;
var options = fixture.Fixture.CreateOptions();
options.SchemaName = fixture.SchemaName;
var dataSource = new AuthorityDataSource(Options.Create(options), NullLogger<AuthorityDataSource>.Instance);
_repository = new OfflineKitAuditRepository(dataSource, NullLogger<OfflineKitAuditRepository>.Instance);
}
public Task InitializeAsync() => _fixture.TruncateAllTablesAsync();
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task Insert_ThenList_ReturnsRecord()
{
var tenantId = Guid.NewGuid().ToString("N");
var entity = new OfflineKitAuditEntity
{
EventId = Guid.NewGuid(),
TenantId = tenantId,
EventType = "IMPORT_VALIDATED",
Timestamp = DateTimeOffset.UtcNow,
Actor = "system",
Details = """{"kitFilename":"bundle-2025-12-14.tar.zst"}""",
Result = "success"
};
await _repository.InsertAsync(entity);
var listed = await _repository.ListAsync(tenantId, limit: 10);
listed.Should().ContainSingle();
listed[0].EventId.Should().Be(entity.EventId);
listed[0].EventType.Should().Be(entity.EventType);
listed[0].Actor.Should().Be(entity.Actor);
listed[0].Result.Should().Be(entity.Result);
listed[0].Details.Should().Contain("kitFilename");
}
[Fact]
public async Task List_WithFilters_ReturnsMatchingRows()
{
var tenantId = Guid.NewGuid().ToString("N");
await _repository.InsertAsync(new OfflineKitAuditEntity
{
EventId = Guid.NewGuid(),
TenantId = tenantId,
EventType = "IMPORT_FAILED_DSSE",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-2),
Actor = "system",
Details = """{"reasonCode":"DSSE_VERIFY_FAIL"}""",
Result = "failed"
});
await _repository.InsertAsync(new OfflineKitAuditEntity
{
EventId = Guid.NewGuid(),
TenantId = tenantId,
EventType = "IMPORT_VALIDATED",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-1),
Actor = "system",
Details = """{"status":"ok"}""",
Result = "success"
});
var failed = await _repository.ListAsync(tenantId, result: "failed", limit: 10);
failed.Should().ContainSingle();
failed[0].Result.Should().Be("failed");
var validated = await _repository.ListAsync(tenantId, eventType: "IMPORT_VALIDATED", limit: 10);
validated.Should().ContainSingle();
validated[0].EventType.Should().Be("IMPORT_VALIDATED");
}
[Fact]
public async Task List_IsTenantIsolated()
{
var tenantA = Guid.NewGuid().ToString("N");
var tenantB = Guid.NewGuid().ToString("N");
await _repository.InsertAsync(new OfflineKitAuditEntity
{
EventId = Guid.NewGuid(),
TenantId = tenantA,
EventType = "IMPORT_VALIDATED",
Timestamp = DateTimeOffset.UtcNow.AddMinutes(-1),
Actor = "system",
Details = """{"status":"ok"}""",
Result = "success"
});
await _repository.InsertAsync(new OfflineKitAuditEntity
{
EventId = Guid.NewGuid(),
TenantId = tenantB,
EventType = "IMPORT_VALIDATED",
Timestamp = DateTimeOffset.UtcNow,
Actor = "system",
Details = """{"status":"ok"}""",
Result = "success"
});
var tenantAResults = await _repository.ListAsync(tenantA, limit: 10);
tenantAResults.Should().ContainSingle();
tenantAResults[0].TenantId.Should().Be(tenantA);
var tenantBResults = await _repository.ListAsync(tenantB, limit: 10);
tenantBResults.Should().ContainSingle();
tenantBResults[0].TenantId.Should().Be(tenantB);
}
}

View File

@@ -4,7 +4,7 @@ using System.Collections.Concurrent;
namespace StellaOps.Authority.Storage.Postgres.Tests.TestDoubles;
internal sealed class InMemoryTokenRepository : ITokenRepository, ISecondaryTokenRepository
internal sealed class InMemoryTokenRepository : ITokenRepository
{
private readonly ConcurrentDictionary<Guid, TokenEntity> _tokens = new();
public bool FailWrites { get; set; }
@@ -67,7 +67,7 @@ internal sealed class InMemoryTokenRepository : ITokenRepository, ISecondaryToke
public IReadOnlyCollection<TokenEntity> Snapshot() => _tokens.Values.ToList();
}
internal sealed class InMemoryRefreshTokenRepository : IRefreshTokenRepository, ISecondaryRefreshTokenRepository
internal sealed class InMemoryRefreshTokenRepository : IRefreshTokenRepository
{
private readonly ConcurrentDictionary<Guid, RefreshTokenEntity> _tokens = new();
public bool FailWrites { get; set; }
@@ -130,7 +130,7 @@ internal sealed class InMemoryRefreshTokenRepository : IRefreshTokenRepository,
public IReadOnlyCollection<RefreshTokenEntity> Snapshot() => _tokens.Values.ToList();
}
internal sealed class InMemoryUserRepository : IUserRepository, ISecondaryUserRepository
internal sealed class InMemoryUserRepository : IUserRepository
{
private readonly ConcurrentDictionary<Guid, UserEntity> _users = new();