feat: Add Bun language analyzer and related functionality

- Implemented BunPackageNormalizer to deduplicate packages by name and version.
- Created BunProjectDiscoverer to identify Bun project roots in the filesystem.
- Added project files for the Bun analyzer including manifest and project configuration.
- Developed comprehensive tests for Bun language analyzer covering various scenarios.
- Included fixture files for testing standard installs, isolated linker installs, lockfile-only scenarios, and workspaces.
- Established stubs for authentication sessions to facilitate testing in the web application.
This commit is contained in:
StellaOps Bot
2025-12-06 11:20:35 +02:00
parent b978ae399f
commit a7cd10020a
85 changed files with 7414 additions and 42 deletions

View File

@@ -0,0 +1,139 @@
using System.Collections.Immutable;
using StellaOps.Concelier.Core.Linksets;
namespace StellaOps.Concelier.WebService.Tests.Cache;
/// <summary>
/// Tests for LNM linkset cache read-through behavior.
/// Per CONCELIER-AIAI-31-002.
/// </summary>
public sealed class LinksetCacheReadThroughTests
{
[Fact]
public void AdvisoryLinkset_CanBeCreatedForCache()
{
var linkset = new AdvisoryLinkset(
TenantId: "test-tenant",
Source: "nvd",
AdvisoryId: "CVE-2024-0001",
ObservationIds: ImmutableArray.Create("obs-1", "obs-2"),
Normalized: new AdvisoryLinksetNormalized(
Purls: new[] { "pkg:npm/lodash@4.17.20" },
Cpes: new[] { "cpe:2.3:a:lodash:lodash:*" },
Versions: new[] { "4.17.20" },
Ranges: null,
Severities: null),
Provenance: new AdvisoryLinksetProvenance(
ObservationHashes: new[] { "sha256:abc123" },
ToolVersion: "1.0.0",
PolicyHash: null),
Confidence: 0.95,
Conflicts: null,
CreatedAt: DateTimeOffset.UtcNow,
BuiltByJobId: "job-123");
Assert.Equal("test-tenant", linkset.TenantId);
Assert.Equal("nvd", linkset.Source);
Assert.Equal("CVE-2024-0001", linkset.AdvisoryId);
Assert.Equal(2, linkset.ObservationIds.Length);
}
[Fact]
public void AdvisoryLinkset_WithConflicts_CanBeCreated()
{
var conflicts = new List<AdvisoryLinksetConflict>
{
new AdvisoryLinksetConflict(
Field: "severity",
Reason: "severity-mismatch",
Values: new[] { "critical", "high" },
SourceIds: new[] { "nvd", "github" })
};
var linkset = new AdvisoryLinkset(
TenantId: "test-tenant",
Source: "aggregated",
AdvisoryId: "CVE-2024-0002",
ObservationIds: ImmutableArray.Create("obs-1"),
Normalized: null,
Provenance: null,
Confidence: 0.72,
Conflicts: conflicts,
CreatedAt: DateTimeOffset.UtcNow,
BuiltByJobId: null);
Assert.NotNull(linkset.Conflicts);
Assert.Single(linkset.Conflicts);
Assert.Equal("severity", linkset.Conflicts[0].Field);
Assert.Equal("severity-mismatch", linkset.Conflicts[0].Reason);
}
[Fact]
public void AdvisoryLinksetNormalized_ContainsExpectedFields()
{
var normalized = new AdvisoryLinksetNormalized(
Purls: new[] { "pkg:npm/example@1.0.0", "pkg:npm/example@1.0.1" },
Cpes: new[] { "cpe:2.3:a:example:*" },
Versions: new[] { "1.0.0", "1.0.1" },
Ranges: new[]
{
new Dictionary<string, object?>
{
["type"] = "SEMVER",
["events"] = new[]
{
new Dictionary<string, object?> { ["introduced"] = "0" },
new Dictionary<string, object?> { ["fixed"] = "1.0.2" }
}
}
},
Severities: new[]
{
new Dictionary<string, object?>
{
["type"] = "CVSS_V3",
["score"] = 9.8
}
});
Assert.NotNull(normalized.Purls);
Assert.Equal(2, normalized.Purls.Count);
Assert.NotNull(normalized.Versions);
Assert.Equal(2, normalized.Versions.Count);
Assert.NotNull(normalized.Ranges);
Assert.Single(normalized.Ranges);
}
[Fact]
public void AdvisoryLinksetProvenance_ContainsHashes()
{
var provenance = new AdvisoryLinksetProvenance(
ObservationHashes: new[] { "sha256:abc123", "sha256:def456" },
ToolVersion: "concelier-v1.0.0",
PolicyHash: "sha256:policy789");
Assert.Equal(2, provenance.ObservationHashes!.Count);
Assert.Equal("concelier-v1.0.0", provenance.ToolVersion);
Assert.Equal("sha256:policy789", provenance.PolicyHash);
}
[Fact]
public void CacheKey_DeterministicFromLinkset()
{
// Cache key should be deterministic: {tenant}:{advisoryId}:{source}
var linkset = new AdvisoryLinkset(
TenantId: "acme",
Source: "nvd",
AdvisoryId: "CVE-2024-0001",
ObservationIds: ImmutableArray<string>.Empty,
Normalized: null,
Provenance: null,
Confidence: null,
Conflicts: null,
CreatedAt: DateTimeOffset.UtcNow,
BuiltByJobId: null);
var cacheKey = $"{linkset.TenantId}:{linkset.AdvisoryId}:{linkset.Source}";
Assert.Equal("acme:CVE-2024-0001:nvd", cacheKey);
}
}

View File

@@ -0,0 +1,117 @@
using StellaOps.Concelier.WebService.Deprecation;
namespace StellaOps.Concelier.WebService.Tests.Deprecation;
/// <summary>
/// Tests for deprecation headers infrastructure.
/// Per CONCELIER-WEB-OAS-63-001.
/// </summary>
public sealed class DeprecationHeadersTests
{
[Fact]
public void DeprecationInfo_LegacyLinksets_HasCorrectValues()
{
var info = DeprecatedEndpoints.LegacyLinksets;
Assert.Equal(DeprecatedEndpoints.LegacyApisDeprecatedAt, info.DeprecatedAt);
Assert.Equal(DeprecatedEndpoints.LegacyApisSunsetAt, info.SunsetAt);
Assert.Equal("/v1/lnm/linksets", info.SuccessorUri);
Assert.NotEmpty(info.Message);
Assert.NotNull(info.MigrationGuideUrl);
}
[Fact]
public void DeprecationInfo_LegacyAdvisoryObservations_HasCorrectValues()
{
var info = DeprecatedEndpoints.LegacyAdvisoryObservations;
Assert.Equal(DeprecatedEndpoints.LegacyApisDeprecatedAt, info.DeprecatedAt);
Assert.Equal(DeprecatedEndpoints.LegacyApisSunsetAt, info.SunsetAt);
Assert.Equal("/v1/lnm/linksets", info.SuccessorUri);
Assert.Contains("includeObservations", info.Message);
}
[Fact]
public void DeprecationInfo_LegacyAdvisoryLinksets_HasCorrectValues()
{
var info = DeprecatedEndpoints.LegacyAdvisoryLinksets;
Assert.Equal(DeprecatedEndpoints.LegacyApisDeprecatedAt, info.DeprecatedAt);
Assert.Equal("/v1/lnm/linksets", info.SuccessorUri);
}
[Fact]
public void DeprecationInfo_LegacyAdvisoryLinksetsExport_HasCorrectValues()
{
var info = DeprecatedEndpoints.LegacyAdvisoryLinksetsExport;
Assert.Equal(DeprecatedEndpoints.LegacyApisDeprecatedAt, info.DeprecatedAt);
Assert.Equal("/v1/lnm/linksets", info.SuccessorUri);
Assert.Contains("pagination", info.Message);
}
[Fact]
public void DeprecationInfo_LegacyConcelierObservations_HasCorrectValues()
{
var info = DeprecatedEndpoints.LegacyConcelierObservations;
Assert.Equal(DeprecatedEndpoints.LegacyApisDeprecatedAt, info.DeprecatedAt);
Assert.Equal("/v1/lnm/linksets", info.SuccessorUri);
}
[Fact]
public void AllDeprecatedEndpoints_HaveMigrationGuides()
{
var endpoints = new[]
{
DeprecatedEndpoints.LegacyLinksets,
DeprecatedEndpoints.LegacyAdvisoryObservations,
DeprecatedEndpoints.LegacyAdvisoryLinksets,
DeprecatedEndpoints.LegacyAdvisoryLinksetsExport,
DeprecatedEndpoints.LegacyConcelierObservations
};
foreach (var endpoint in endpoints)
{
Assert.NotNull(endpoint.MigrationGuideUrl);
Assert.StartsWith(DeprecatedEndpoints.MigrationGuideBaseUrl, endpoint.MigrationGuideUrl);
}
}
[Fact]
public void AllDeprecatedEndpoints_HaveSunsetDates()
{
var endpoints = new[]
{
DeprecatedEndpoints.LegacyLinksets,
DeprecatedEndpoints.LegacyAdvisoryObservations,
DeprecatedEndpoints.LegacyAdvisoryLinksets,
DeprecatedEndpoints.LegacyAdvisoryLinksetsExport,
DeprecatedEndpoints.LegacyConcelierObservations
};
foreach (var endpoint in endpoints)
{
Assert.NotNull(endpoint.SunsetAt);
Assert.True(endpoint.SunsetAt > endpoint.DeprecatedAt);
}
}
[Fact]
public void SunsetDate_IsAfterDeprecationDate()
{
Assert.True(
DeprecatedEndpoints.LegacyApisSunsetAt > DeprecatedEndpoints.LegacyApisDeprecatedAt,
"Sunset date must be after deprecation date");
}
[Fact]
public void DeprecationHeaders_ConstantsAreDefined()
{
Assert.Equal("Deprecation", DeprecationHeaders.Deprecation);
Assert.Equal("Sunset", DeprecationHeaders.Sunset);
Assert.Equal("Link", DeprecationHeaders.Link);
Assert.Equal("X-Deprecation-Notice", DeprecationHeaders.XDeprecationNotice);
Assert.Equal("X-Deprecation-Guide", DeprecationHeaders.XDeprecationGuide);
}
}