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
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:
@@ -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());
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user