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,12 @@
namespace StellaOps.Policy.Engine.AirGap;
/// <summary>
/// Store for imported policy pack bundles.
/// </summary>
public interface IPolicyPackBundleStore
{
Task<ImportedPolicyPackBundle?> GetAsync(string bundleId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<ImportedPolicyPackBundle>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default);
Task SaveAsync(ImportedPolicyPackBundle bundle, CancellationToken cancellationToken = default);
Task DeleteAsync(string bundleId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,47 @@
using System.Collections.Concurrent;
namespace StellaOps.Policy.Engine.AirGap;
/// <summary>
/// In-memory implementation of policy pack bundle store.
/// </summary>
internal sealed class InMemoryPolicyPackBundleStore : IPolicyPackBundleStore
{
private readonly ConcurrentDictionary<string, ImportedPolicyPackBundle> _bundles = new(StringComparer.Ordinal);
public Task<ImportedPolicyPackBundle?> GetAsync(string bundleId, CancellationToken cancellationToken = default)
{
_bundles.TryGetValue(bundleId, out var bundle);
return Task.FromResult(bundle);
}
public Task<IReadOnlyList<ImportedPolicyPackBundle>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
{
IEnumerable<ImportedPolicyPackBundle> bundles = _bundles.Values;
if (!string.IsNullOrWhiteSpace(tenantId))
{
bundles = bundles.Where(b => string.Equals(b.TenantId, tenantId, StringComparison.Ordinal));
}
var ordered = bundles
.OrderByDescending(b => b.ImportedAt)
.ThenBy(b => b.BundleId, StringComparer.Ordinal)
.ToList();
return Task.FromResult<IReadOnlyList<ImportedPolicyPackBundle>>(ordered);
}
public Task SaveAsync(ImportedPolicyPackBundle bundle, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(bundle);
_bundles[bundle.BundleId] = bundle;
return Task.CompletedTask;
}
public Task DeleteAsync(string bundleId, CancellationToken cancellationToken = default)
{
_bundles.TryRemove(bundleId, out _);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,248 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
namespace StellaOps.Policy.Engine.AirGap;
/// <summary>
/// Service for importing policy pack bundles per CONTRACT-MIRROR-BUNDLE-003.
/// </summary>
internal sealed class PolicyPackBundleImportService
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private readonly IPolicyPackBundleStore _store;
private readonly TimeProvider _timeProvider;
private readonly ILogger<PolicyPackBundleImportService> _logger;
public PolicyPackBundleImportService(
IPolicyPackBundleStore store,
TimeProvider timeProvider,
ILogger<PolicyPackBundleImportService> logger)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Registers a bundle for import and begins validation.
/// </summary>
public async Task<RegisterBundleResponse> RegisterBundleAsync(
string tenantId,
RegisterBundleRequest request,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentNullException.ThrowIfNull(request);
ArgumentException.ThrowIfNullOrWhiteSpace(request.BundlePath);
var now = _timeProvider.GetUtcNow();
var importId = GenerateImportId();
_logger.LogInformation("Registering bundle import {ImportId} from {BundlePath} for tenant {TenantId}",
importId, request.BundlePath, tenantId);
// Create initial entry in validating state
var entry = new ImportedPolicyPackBundle(
BundleId: importId,
DomainId: BundleDomainIds.PolicyPacks,
TenantId: tenantId,
Status: BundleImportStatus.Validating,
ExportCount: 0,
ImportedAt: now.ToString("O"),
Error: null,
Bundle: null);
await _store.SaveAsync(entry, cancellationToken).ConfigureAwait(false);
// Start async import process
_ = ImportBundleAsync(tenantId, importId, request, cancellationToken);
return new RegisterBundleResponse(importId, BundleImportStatus.Validating);
}
/// <summary>
/// Gets the status of a bundle import.
/// </summary>
public async Task<BundleStatusResponse?> GetBundleStatusAsync(
string bundleId,
CancellationToken cancellationToken = default)
{
var bundle = await _store.GetAsync(bundleId, cancellationToken).ConfigureAwait(false);
if (bundle is null)
{
return null;
}
return new BundleStatusResponse(
BundleId: bundle.BundleId,
DomainId: bundle.DomainId,
Status: bundle.Status,
ExportCount: bundle.ExportCount,
ImportedAt: bundle.ImportedAt,
Error: bundle.Error);
}
/// <summary>
/// Lists imported bundles for a tenant.
/// </summary>
public async Task<IReadOnlyList<BundleStatusResponse>> ListBundlesAsync(
string? tenantId = null,
CancellationToken cancellationToken = default)
{
var bundles = await _store.ListAsync(tenantId, cancellationToken).ConfigureAwait(false);
return bundles.Select(b => new BundleStatusResponse(
BundleId: b.BundleId,
DomainId: b.DomainId,
Status: b.Status,
ExportCount: b.ExportCount,
ImportedAt: b.ImportedAt,
Error: b.Error)).ToList();
}
private async Task ImportBundleAsync(
string tenantId,
string importId,
RegisterBundleRequest request,
CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Starting bundle import {ImportId}", importId);
// Update status to importing
var current = await _store.GetAsync(importId, cancellationToken).ConfigureAwait(false);
if (current is null)
{
return;
}
await _store.SaveAsync(current with { Status = BundleImportStatus.Importing }, cancellationToken).ConfigureAwait(false);
// Load and parse bundle
var bundle = await LoadBundleAsync(request.BundlePath, cancellationToken).ConfigureAwait(false);
// Validate bundle
ValidateBundle(bundle);
// Verify signatures if present
if (bundle.Signature is not null)
{
await VerifySignatureAsync(bundle, request.TrustRootsPath, cancellationToken).ConfigureAwait(false);
}
// Verify export digests
VerifyExportDigests(bundle);
// Mark as imported
var now = _timeProvider.GetUtcNow();
var imported = new ImportedPolicyPackBundle(
BundleId: importId,
DomainId: bundle.DomainId,
TenantId: tenantId,
Status: BundleImportStatus.Imported,
ExportCount: bundle.Exports.Count,
ImportedAt: now.ToString("O"),
Error: null,
Bundle: bundle);
await _store.SaveAsync(imported, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Bundle import {ImportId} completed successfully with {ExportCount} exports",
importId, bundle.Exports.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Bundle import {ImportId} failed: {Error}", importId, ex.Message);
var failed = await _store.GetAsync(importId, CancellationToken.None).ConfigureAwait(false);
if (failed is not null)
{
await _store.SaveAsync(failed with
{
Status = BundleImportStatus.Failed,
Error = ex.Message
}, CancellationToken.None).ConfigureAwait(false);
}
}
}
private static async Task<PolicyPackBundle> LoadBundleAsync(string bundlePath, CancellationToken cancellationToken)
{
if (!File.Exists(bundlePath))
{
throw new FileNotFoundException($"Bundle file not found: {bundlePath}");
}
var json = await File.ReadAllTextAsync(bundlePath, cancellationToken).ConfigureAwait(false);
var bundle = JsonSerializer.Deserialize<PolicyPackBundle>(json, JsonOptions)
?? throw new InvalidDataException("Failed to parse bundle JSON");
return bundle;
}
private static void ValidateBundle(PolicyPackBundle bundle)
{
if (bundle.SchemaVersion < 1)
{
throw new InvalidDataException("Invalid schema version");
}
if (string.IsNullOrWhiteSpace(bundle.DomainId))
{
throw new InvalidDataException("Domain ID is required");
}
if (bundle.Exports.Count == 0)
{
throw new InvalidDataException("Bundle must contain at least one export");
}
foreach (var export in bundle.Exports)
{
if (string.IsNullOrWhiteSpace(export.Key))
{
throw new InvalidDataException("Export key is required");
}
if (string.IsNullOrWhiteSpace(export.ArtifactDigest))
{
throw new InvalidDataException($"Artifact digest is required for export '{export.Key}'");
}
}
}
private Task VerifySignatureAsync(PolicyPackBundle bundle, string? trustRootsPath, CancellationToken cancellationToken)
{
// Signature verification would integrate with the AirGap.Importer DsseVerifier
// For now, log that signature is present
_logger.LogInformation("Bundle signature present: algorithm={Algorithm}, keyId={KeyId}",
bundle.Signature!.Algorithm, bundle.Signature.KeyId);
return Task.CompletedTask;
}
private void VerifyExportDigests(PolicyPackBundle bundle)
{
foreach (var export in bundle.Exports)
{
// Verify digest format
if (!export.ArtifactDigest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException($"Invalid digest format for export '{export.Key}': expected sha256: prefix");
}
_logger.LogDebug("Verified export '{Key}' with digest {Digest}",
export.Key, export.ArtifactDigest);
}
}
private static string GenerateImportId()
{
return $"import-{Guid.NewGuid():N}"[..20];
}
}

View File

@@ -0,0 +1,113 @@
using System.Text.Json.Serialization;
namespace StellaOps.Policy.Engine.AirGap;
/// <summary>
/// Mirror bundle for policy packs per CONTRACT-MIRROR-BUNDLE-003.
/// </summary>
public sealed record PolicyPackBundle(
[property: JsonPropertyName("schemaVersion")] int SchemaVersion,
[property: JsonPropertyName("generatedAt")] string GeneratedAt,
[property: JsonPropertyName("targetRepository")] string? TargetRepository,
[property: JsonPropertyName("domainId")] string DomainId,
[property: JsonPropertyName("displayName")] string? DisplayName,
[property: JsonPropertyName("exports")] IReadOnlyList<PolicyPackExport> Exports,
[property: JsonPropertyName("signature")] BundleSignature? Signature);
/// <summary>
/// Export entry within a policy pack bundle.
/// </summary>
public sealed record PolicyPackExport(
[property: JsonPropertyName("key")] string Key,
[property: JsonPropertyName("format")] string Format,
[property: JsonPropertyName("exportId")] string ExportId,
[property: JsonPropertyName("querySignature")] string? QuerySignature,
[property: JsonPropertyName("createdAt")] string CreatedAt,
[property: JsonPropertyName("artifactSizeBytes")] long ArtifactSizeBytes,
[property: JsonPropertyName("artifactDigest")] string ArtifactDigest,
[property: JsonPropertyName("sourceProviders")] IReadOnlyList<string>? SourceProviders,
[property: JsonPropertyName("consensusRevision")] string? ConsensusRevision,
[property: JsonPropertyName("policyRevisionId")] string? PolicyRevisionId,
[property: JsonPropertyName("policyDigest")] string? PolicyDigest,
[property: JsonPropertyName("consensusDigest")] string? ConsensusDigest,
[property: JsonPropertyName("scoreDigest")] string? ScoreDigest,
[property: JsonPropertyName("attestation")] AttestationDescriptor? Attestation);
/// <summary>
/// Attestation metadata for signed exports.
/// </summary>
public sealed record AttestationDescriptor(
[property: JsonPropertyName("predicateType")] string PredicateType,
[property: JsonPropertyName("rekorLocation")] string? RekorLocation,
[property: JsonPropertyName("envelopeDigest")] string? EnvelopeDigest,
[property: JsonPropertyName("signedAt")] string SignedAt);
/// <summary>
/// Bundle signature metadata.
/// </summary>
public sealed record BundleSignature(
[property: JsonPropertyName("path")] string Path,
[property: JsonPropertyName("algorithm")] string Algorithm,
[property: JsonPropertyName("keyId")] string KeyId,
[property: JsonPropertyName("provider")] string? Provider,
[property: JsonPropertyName("signedAt")] string SignedAt);
/// <summary>
/// Request to register a bundle for import.
/// </summary>
public sealed record RegisterBundleRequest(
[property: JsonPropertyName("bundlePath")] string BundlePath,
[property: JsonPropertyName("trustRootsPath")] string? TrustRootsPath);
/// <summary>
/// Response for bundle registration.
/// </summary>
public sealed record RegisterBundleResponse(
[property: JsonPropertyName("importId")] string ImportId,
[property: JsonPropertyName("status")] string Status);
/// <summary>
/// Bundle import status response.
/// </summary>
public sealed record BundleStatusResponse(
[property: JsonPropertyName("bundleId")] string BundleId,
[property: JsonPropertyName("domainId")] string DomainId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("exportCount")] int ExportCount,
[property: JsonPropertyName("importedAt")] string? ImportedAt,
[property: JsonPropertyName("error")] string? Error);
/// <summary>
/// Imported bundle catalog entry.
/// </summary>
public sealed record ImportedPolicyPackBundle(
string BundleId,
string DomainId,
string TenantId,
string Status,
int ExportCount,
string ImportedAt,
string? Error,
PolicyPackBundle? Bundle);
/// <summary>
/// Bundle import status values.
/// </summary>
public static class BundleImportStatus
{
public const string Validating = "validating";
public const string Importing = "importing";
public const string Imported = "imported";
public const string Failed = "failed";
}
/// <summary>
/// Domain IDs per CONTRACT-MIRROR-BUNDLE-003.
/// </summary>
public static class BundleDomainIds
{
public const string VexAdvisories = "vex-advisories";
public const string VulnerabilityFeeds = "vulnerability-feeds";
public const string PolicyPacks = "policy-packs";
public const string SbomCatalog = "sbom-catalog";
}