Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,142 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using StellaOps.Scheduler.ImpactIndex;
using StellaOps.Scheduler.Models;
using Xunit;
namespace StellaOps.Scheduler.ImpactIndex.Tests;
public sealed class FixtureImpactIndexTests
{
[Fact]
public async Task ResolveByPurls_UsesEmbeddedFixtures()
{
var selector = new Selector(SelectorScope.AllImages);
var (impactIndex, loggerFactory) = CreateImpactIndex();
using var _ = loggerFactory;
var result = await impactIndex.ResolveByPurlsAsync(
new[] { "pkg:apk/alpine/openssl@3.2.2-r0?arch=x86_64" },
usageOnly: false,
selector);
result.UsageOnly.Should().BeFalse();
result.Images.Should().ContainSingle();
var image = result.Images.Single();
image.ImageDigest.Should().Be("sha256:8f47d7c6b538c0d9533b78913cba3d5e671e7c4b4e7c6a2bb9a1a1c4d4f8e123");
image.Registry.Should().Be("docker.io");
image.Repository.Should().Be("library/nginx");
image.Tags.Should().ContainSingle(tag => tag == "1.25.4");
image.UsedByEntrypoint.Should().BeTrue();
result.GeneratedAt.Should().Be(DateTimeOffset.Parse("2025-10-19T00:00:00Z"));
result.SchemaVersion.Should().Be(SchedulerSchemaVersions.ImpactSet);
}
[Fact]
public async Task ResolveByPurls_UsageOnlyFiltersInventoryOnlyComponents()
{
var selector = new Selector(SelectorScope.AllImages);
var (impactIndex, loggerFactory) = CreateImpactIndex();
using var _ = loggerFactory;
var inventoryOnlyPurl = "pkg:apk/alpine/pcre2@10.42-r1?arch=x86_64";
var runtimeResult = await impactIndex.ResolveByPurlsAsync(
new[] { inventoryOnlyPurl },
usageOnly: true,
selector);
runtimeResult.Images.Should().BeEmpty();
var inventoryResult = await impactIndex.ResolveByPurlsAsync(
new[] { inventoryOnlyPurl },
usageOnly: false,
selector);
inventoryResult.Images.Should().ContainSingle();
inventoryResult.Images.Single().UsedByEntrypoint.Should().BeFalse();
}
[Fact]
public async Task ResolveAll_ReturnsDeterministicFixtureSet()
{
var selector = new Selector(SelectorScope.AllImages);
var (impactIndex, loggerFactory) = CreateImpactIndex();
using var _ = loggerFactory;
var first = await impactIndex.ResolveAllAsync(selector, usageOnly: false);
first.Images.Should().HaveCount(6);
var second = await impactIndex.ResolveAllAsync(selector, usageOnly: false);
second.Images.Should().HaveCount(6);
second.Images.Should().Equal(first.Images);
}
[Fact]
public async Task ResolveByVulnerabilities_ReturnsEmptySet()
{
var selector = new Selector(SelectorScope.AllImages);
var (impactIndex, loggerFactory) = CreateImpactIndex();
using var _ = loggerFactory;
var result = await impactIndex.ResolveByVulnerabilitiesAsync(
new[] { "CVE-2025-0001" },
usageOnly: false,
selector);
result.Images.Should().BeEmpty();
}
[Fact]
public async Task FixtureDirectoryOption_LoadsFromFileSystem()
{
var selector = new Selector(SelectorScope.AllImages);
var samplesDirectory = LocateSamplesDirectory();
var (impactIndex, loggerFactory) = CreateImpactIndex(options =>
{
options.FixtureDirectory = samplesDirectory;
});
using var _ = loggerFactory;
var result = await impactIndex.ResolveAllAsync(selector, usageOnly: false);
result.Images.Should().HaveCount(6);
}
private static (FixtureImpactIndex ImpactIndex, ILoggerFactory LoggerFactory) CreateImpactIndex(
Action<ImpactIndexStubOptions>? configure = null)
{
var options = new ImpactIndexStubOptions();
configure?.Invoke(options);
var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
var logger = loggerFactory.CreateLogger<FixtureImpactIndex>();
var impactIndex = new FixtureImpactIndex(options, TimeProvider.System, logger);
return (impactIndex, loggerFactory);
}
private static string LocateSamplesDirectory()
{
var current = AppContext.BaseDirectory;
while (!string.IsNullOrWhiteSpace(current))
{
var candidate = Path.Combine(current, "samples", "scanner", "images");
if (Directory.Exists(candidate))
{
return candidate;
}
current = Directory.GetParent(current)?.FullName;
}
throw new InvalidOperationException("Unable to locate 'samples/scanner/images'.");
}
}

View File

@@ -0,0 +1,195 @@
using System;
using System.Collections.Immutable;
using System.IO;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scheduler.ImpactIndex.Ingestion;
using StellaOps.Scheduler.Models;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Emit.Index;
using Xunit;
namespace StellaOps.Scheduler.ImpactIndex.Tests;
public sealed class RoaringImpactIndexTests
{
[Fact]
public async Task IngestAsync_RegistersComponentsAndUsage()
{
var (stream, digest) = CreateBomIndex(
ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0"),
ComponentUsage.Create(true, new[] { "/app/start.sh" }));
var index = new RoaringImpactIndex(NullLogger<RoaringImpactIndex>.Instance);
var request = new ImpactIndexIngestionRequest
{
TenantId = "tenant-alpha",
ImageDigest = digest,
Registry = "docker.io",
Repository = "library/alpine",
Namespaces = ImmutableArray.Create("team-a"),
Tags = ImmutableArray.Create("3.20"),
Labels = ImmutableSortedDictionary.CreateRange(StringComparer.OrdinalIgnoreCase, new[]
{
new KeyValuePair<string, string>("env", "prod")
}),
BomIndexStream = stream,
};
await index.IngestAsync(request, CancellationToken.None);
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
var impactSet = await index.ResolveByPurlsAsync(new[] { "pkg:npm/a@1.0.0" }, usageOnly: false, selector);
impactSet.Images.Should().HaveCount(1);
impactSet.Images[0].ImageDigest.Should().Be(digest);
impactSet.Images[0].Tags.Should().ContainSingle(tag => tag == "3.20");
impactSet.Images[0].UsedByEntrypoint.Should().BeTrue();
var usageOnly = await index.ResolveByPurlsAsync(new[] { "pkg:npm/a@1.0.0" }, usageOnly: true, selector);
usageOnly.Images.Should().HaveCount(1);
}
[Fact]
public async Task IngestAsync_ReplacesExistingImageData()
{
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
var (initialStream, digest) = CreateBomIndex(component, ComponentUsage.Create(false));
var index = new RoaringImpactIndex(NullLogger<RoaringImpactIndex>.Instance);
await index.IngestAsync(new ImpactIndexIngestionRequest
{
TenantId = "tenant-alpha",
ImageDigest = digest,
Registry = "docker.io",
Repository = "library/alpine",
Tags = ImmutableArray.Create("v1"),
BomIndexStream = initialStream,
});
var (updatedStream, _) = CreateBomIndex(component, ComponentUsage.Create(true, new[] { "/start.sh" }), digest);
await index.IngestAsync(new ImpactIndexIngestionRequest
{
TenantId = "tenant-alpha",
ImageDigest = digest,
Registry = "docker.io",
Repository = "library/alpine",
Tags = ImmutableArray.Create("v2"),
BomIndexStream = updatedStream,
});
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
var impactSet = await index.ResolveByPurlsAsync(new[] { "pkg:npm/a@1.0.0" }, usageOnly: true, selector);
impactSet.Images.Should().HaveCount(1);
impactSet.Images[0].Tags.Should().ContainSingle(tag => tag == "v2");
impactSet.Images[0].UsedByEntrypoint.Should().BeTrue();
}
[Fact]
public async Task ResolveByPurlsAsync_RespectsTenantNamespaceAndTagFilters()
{
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
var (tenantStream, tenantDigest) = CreateBomIndex(component, ComponentUsage.Create(true, new[] { "/start.sh" }));
var (otherStream, otherDigest) = CreateBomIndex(component, ComponentUsage.Create(false));
var index = new RoaringImpactIndex(NullLogger<RoaringImpactIndex>.Instance);
await index.IngestAsync(new ImpactIndexIngestionRequest
{
TenantId = "tenant-alpha",
ImageDigest = tenantDigest,
Registry = "docker.io",
Repository = "library/service",
Namespaces = ImmutableArray.Create("team-alpha"),
Tags = ImmutableArray.Create("prod-eu"),
BomIndexStream = tenantStream,
});
await index.IngestAsync(new ImpactIndexIngestionRequest
{
TenantId = "tenant-beta",
ImageDigest = otherDigest,
Registry = "docker.io",
Repository = "library/service",
Namespaces = ImmutableArray.Create("team-beta"),
Tags = ImmutableArray.Create("staging-us"),
BomIndexStream = otherStream,
});
var selector = new Selector(
SelectorScope.AllImages,
tenantId: "tenant-alpha",
namespaces: new[] { "team-alpha" },
includeTags: new[] { "prod-*" });
var result = await index.ResolveByPurlsAsync(new[] { "pkg:npm/a@1.0.0" }, usageOnly: true, selector);
result.Images.Should().ContainSingle(image => image.ImageDigest == tenantDigest);
result.Images[0].Tags.Should().Contain("prod-eu");
}
[Fact]
public async Task ResolveAllAsync_UsageOnlyFiltersEntrypointImages()
{
var component = ComponentIdentity.Create("pkg:npm/a@1.0.0", "a", "1.0.0", "pkg:npm/a@1.0.0");
var (entryStream, entryDigest) = CreateBomIndex(component, ComponentUsage.Create(true, new[] { "/start.sh" }));
var nonEntryDigestValue = "sha256:" + new string('1', 64);
var (nonEntryStream, nonEntryDigest) = CreateBomIndex(component, ComponentUsage.Create(false), nonEntryDigestValue);
var index = new RoaringImpactIndex(NullLogger<RoaringImpactIndex>.Instance);
await index.IngestAsync(new ImpactIndexIngestionRequest
{
TenantId = "tenant-alpha",
ImageDigest = entryDigest,
Registry = "docker.io",
Repository = "library/service",
BomIndexStream = entryStream,
});
await index.IngestAsync(new ImpactIndexIngestionRequest
{
TenantId = "tenant-alpha",
ImageDigest = nonEntryDigest,
Registry = "docker.io",
Repository = "library/service",
BomIndexStream = nonEntryStream,
});
var selector = new Selector(SelectorScope.AllImages, tenantId: "tenant-alpha");
var usageOnly = await index.ResolveAllAsync(selector, usageOnly: true);
usageOnly.Images.Should().ContainSingle(image => image.ImageDigest == entryDigest);
var allImages = await index.ResolveAllAsync(selector, usageOnly: false);
allImages.Images.Should().HaveCount(2);
}
private static (Stream Stream, string Digest) CreateBomIndex(ComponentIdentity identity, ComponentUsage usage, string? digest = null)
{
var layer = LayerComponentFragment.Create(
"sha256:layer1",
new[]
{
new ComponentRecord
{
Identity = identity,
LayerDigest = "sha256:layer1",
Usage = usage,
}
});
var graph = ComponentGraphBuilder.Build(new[] { layer });
var effectiveDigest = digest ?? "sha256:" + Guid.NewGuid().ToString("N");
var builder = new BomIndexBuilder();
var artifact = builder.Build(new BomIndexBuildRequest
{
ImageDigest = effectiveDigest,
Graph = graph,
GeneratedAt = DateTimeOffset.UtcNow,
});
return (new MemoryStream(artifact.Bytes, writable: false), effectiveDigest);
}
}

View File

@@ -0,0 +1,27 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
<RestoreIgnoreFailedSources>true</RestoreIgnoreFailedSources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scheduler.ImpactIndex/StellaOps.Scheduler.ImpactIndex.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scheduler.Models/StellaOps.Scheduler.Models.csproj" />
<ProjectReference Include="../../../Scanner/__Libraries/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj" />
</ItemGroup>
</Project>