Files
jobtrackingapp/docs/audits/2026-09-07/validation/BoundaryProbes.cs
T
cesnimda 0954876295 fix(security): enforce audited account, network and resource boundaries
Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
2026-09-09 18:21:47 +02:00

153 lines
9.5 KiB
C#

using System.Data.Common;
using System.Net;
using System.Text.Json;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using JobTrackerApi.Services.JobImport;
using JobTrackerApi.Services.JobImport.Translation;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moq;
namespace IndependentAuditValidation;
public static class Entry
{
public static async Task Main(string[] args)
{
if (args.Any(arg => arg.StartsWith("--print-to-pdf=")))
{
// Inert browser substitute. It opens no network connection and renders no HTML.
var root = Environment.GetEnvironmentVariable("VALIDATION_FAKE_BROWSER_ROOT")!;
await File.WriteAllTextAsync(Path.Combine(root, Environment.ProcessId + ".started"), "started");
for (var i = 0; i < 100 && !File.Exists(Path.Combine(root, "release")); i++) await Task.Delay(50);
await File.WriteAllTextAsync(args.Single(arg => arg.StartsWith("--print-to-pdf="))[15..], "%PDF-synthetic-launch-probe");
return;
}
var output = new List<object>();
foreach (var target in new[] { "http://10.1.2.3/", "http://[::ffff:10.1.2.3]/", "http://169.254.169.254/", "http://[::ffff:169.254.169.254]/", "http://192.168.1.1/", "http://[::ffff:192.168.1.1]/", "http://[::1]/", "http://[::ffff:127.0.0.1]/" })
{
var handler = new RecordingHandler();
var clientFactory = new Mock<IHttpClientFactory>();
clientFactory.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(new HttpClient(handler));
var resolver = new Mock<IHostAddressResolver>();
resolver.Setup(x => x.ResolveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync([IPAddress.Parse("93.184.216.34")]);
var service = new JobImportService(clientFactory.Object, new UniversalJobParser(), Array.Empty<IJobSitePlugin>(), new NoOpTranslationService(), resolver.Object);
var result = await service.PreviewAsync(target, default);
output.Add(new { probe = "URL guard with non-network HTTP fake", target, handler.Calls, result.Parser, result.Error });
}
var folder = Path.Combine(Path.GetTempPath(), "jobjakt-boundary-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(folder);
var fakeRoot = Path.Combine(folder, "fake-browser");
Directory.CreateDirectory(fakeRoot);
var executable = Path.ChangeExtension(typeof(Entry).Assembly.Location, OperatingSystem.IsWindows() ? ".exe" : null);
Environment.SetEnvironmentVariable("CV_PDF_BROWSER_PATH", executable);
Environment.SetEnvironmentVariable("VALIDATION_FAKE_BROWSER_ROOT", fakeRoot);
var pdfConfig = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?> { ["Data:Root"] = folder }).Build();
var environment = new Mock<IHostEnvironment>();
environment.SetupGet(x => x.ContentRootPath).Returns(folder);
var pdf = new PlaywrightCvPdfExporter(new AppPaths(pdfConfig, environment.Object), Microsoft.Extensions.Logging.Abstractions.NullLogger<PlaywrightCvPdfExporter>.Instance, pdfConfig);
var renders = Enumerable.Range(0, 3).Select(i => pdf.ExportAsync("synthetic-owner", new TailoredCvRenderResult("synthetic", "synthetic.pdf", "<p>Inert</p>"), default)).ToArray();
for (var i = 0; i < 100 && Directory.GetFiles(fakeRoot, "*.started").Length < 3; i++) await Task.Delay(50);
var concurrentLaunches = Directory.GetFiles(fakeRoot, "*.started").Length;
await File.WriteAllTextAsync(Path.Combine(fakeRoot, "release"), "release");
await Task.WhenAll(renders);
output.Add(new { probe = "actual PDF exporter with three inert child processes", concurrentLaunches });
var connectionString = "Data Source=" + Path.Combine(folder, "independent.db");
var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(connectionString).Options;
using (var db = new JobTrackerContext(options, new FixedOwner()))
{
await db.Database.EnsureCreatedAsync();
db.Users.Add(new ApplicationUser { Id = "synthetic-owner", UserName = "owner@example.test", NormalizedUserName = "OWNER@EXAMPLE.TEST" });
await db.SaveChangesAsync();
}
var career = new Mock<ICareerProfileService>().Object;
var renderer = new Mock<IThemedCvRenderer>().Object;
int id;
using (var db = new JobTrackerContext(options, new FixedOwner()))
id = (await new CvVariantService(db, career, renderer).CreateAsync("synthetic-owner", "Initial", null, new CvVariantSettings(), default)).Id;
using (var dbA = new JobTrackerContext(options, new FixedOwner()))
using (var dbB = new JobTrackerContext(options, new FixedOwner()))
{
var a = new CvVariantService(dbA, career, renderer);
var b = new CvVariantService(dbB, career, renderer);
await a.GetAsync("synthetic-owner", id, default);
await b.GetAsync("synthetic-owner", id, default);
await a.SaveAsync("synthetic-owner", id, "Writer A", new CvVariantSettings(), "a", default);
await b.SaveAsync("synthetic-owner", id, "Writer B", new CvVariantSettings(), "b", default);
}
using (var db = new JobTrackerContext(options, new FixedOwner()))
{
var current = await db.CvVariants.SingleAsync();
var versions = await db.CvVariantVersions.OrderBy(v => v.Id).Select(v => v.Version).ToListAsync();
output.Add(new { probe = "two preloaded real SQLite contexts", current.Name, current.Version, historyVersions = versions });
}
foreach (var concurrency in new[] { 1, 2 })
{
var fault = new OneShotDbFailure();
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["Workers:AiOperationsEnabled"] = "true", ["AiQueue:WorkerConcurrency"] = concurrency.ToString(),
["AiQueue:IdleDelayMs"] = "100"
}).Build();
using var host = new HostBuilder().ConfigureLogging(l => l.ClearProviders()).ConfigureServices(services =>
{
services.AddSingleton<IConfiguration>(config);
services.AddSingleton<TimeProvider>(TimeProvider.System);
services.AddScoped<ICurrentUserService, NeutralOwner>();
services.AddDbContext<JobTrackerContext>(o => o.UseSqlite(connectionString).AddInterceptors(fault));
services.AddScoped<UserOperationStore>();
services.AddSingleton<AiPrivacyPolicy>();
services.AddSingleton<AiOperationExecutionScope>();
services.AddSingleton<IAiOperationHandler, UnusedHandler>();
services.AddSingleton<AiOperationWorker>();
services.AddSingleton<AiOperationHostedService>();
services.AddHostedService(p => p.GetRequiredService<AiOperationHostedService>());
}).Build();
await host.StartAsync();
await Task.Delay(150);
fault.Armed = true;
await Task.Delay(700);
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
var service = host.Services.GetRequiredService<AiOperationHostedService>();
output.Add(new { probe = "one transient database command failure", concurrency, fault.Failures, hostStopping = lifetime.ApplicationStopping.IsCancellationRequested, hostedTaskFaulted = service.ExecuteTask?.IsFaulted, error = service.ExecuteTask?.Exception?.GetBaseException().GetType().Name });
using var shutdown = new CancellationTokenSource(TimeSpan.FromSeconds(3));
await host.StopAsync(shutdown.Token);
}
Console.WriteLine(JsonSerializer.Serialize(output, new JsonSerializerOptions { WriteIndented = true }));
}
}
sealed class FixedOwner : ICurrentUserService { public string? UserId => "synthetic-owner"; }
sealed class NeutralOwner : ICurrentUserService { public string? UserId => null; }
sealed class RecordingHandler : HttpMessageHandler
{
public int Calls;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Calls++;
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("<html><body>Synthetic inert HTML; no sockets are opened.</body></html>") });
}
}
sealed class OneShotDbFailure : DbCommandInterceptor
{
public volatile bool Armed;
private int fired;
public int Failures => fired;
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
{
if (Armed && Interlocked.CompareExchange(ref fired, 1, 0) == 0) throw new TimeoutException("Synthetic one-shot database timeout");
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
}
sealed class UnusedHandler : IAiOperationHandler
{
public string TaskType => "synthetic.validation";
public Task<AiOperationExecutionResult> ExecuteAsync(AiOperationExecutionContext context, IServiceProvider services, CancellationToken cancellationToken) => throw new InvalidOperationException("No operations were queued in this probe.");
}