license switch agpl -> busl1, sprints work, new product advisories

This commit is contained in:
master
2026-01-20 15:32:20 +02:00
parent 4903395618
commit c32fff8f86
1835 changed files with 38630 additions and 4359 deletions

View File

@@ -0,0 +1,17 @@
# StellaOps.Artifact.Core Local Agent Charter
## Scope
- Applies to `src/__Libraries/StellaOps.Artifact.Core/**`.
- Focus on artifact parsing, extraction, and storage contracts.
## Required Reading (treat as read before edits)
- `docs/operations/artifact-migration-runbook.md`
- `docs/modules/platform/architecture-overview.md`
## Working Agreements
- Keep outputs deterministic (ordering, timestamps, hashes).
- Avoid network access; keep parsing offline-friendly.
- Update sprint status in `docs/implplan/SPRINT_*.md` when work starts/finishes.
## Testing Expectations
- Add or update unit tests for behavior changes.

View File

@@ -8,6 +8,7 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using StellaOps.Artifact.Core;
@@ -189,8 +190,8 @@ public sealed class ArtifactController : ControllerBase
[FromQuery] DateTimeOffset? from,
[FromQuery] DateTimeOffset? to,
[FromQuery] int limit = 100,
[FromQuery(Name = "continuation_token")] string? continuationToken,
CancellationToken ct)
[FromQuery(Name = "continuation_token")] string? continuationToken = null,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(bomRef))
{

View File

@@ -5,9 +5,14 @@
// Description: Standalone service for extracting metadata from CycloneDX SBOMs
// -----------------------------------------------------------------------------
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Concelier.SbomIntegration.Models;
using StellaOps.Concelier.SbomIntegration.Parsing;
namespace StellaOps.Artifact.Core;
@@ -26,6 +31,16 @@ public interface ICycloneDxExtractor
/// </summary>
Task<CycloneDxMetadata> ExtractAsync(Stream stream, CancellationToken ct = default);
/// <summary>
/// Extracts enriched SBOM data from a CycloneDX JSON document.
/// </summary>
ParsedSbom ExtractParsed(JsonDocument document);
/// <summary>
/// Extracts enriched SBOM data from a CycloneDX JSON stream.
/// </summary>
Task<ParsedSbom> ExtractParsedAsync(Stream stream, CancellationToken ct = default);
/// <summary>
/// Extracts metadata from a CycloneDX XML document.
/// Sprint: SPRINT_20260118_017 (AS-004)
@@ -95,6 +110,41 @@ public sealed record CycloneDxMetadata
/// </summary>
public sealed class CycloneDxExtractor : ICycloneDxExtractor
{
private readonly IParsedSbomParser _parser;
public CycloneDxExtractor()
: this(new ParsedSbomParser(NullLogger<ParsedSbomParser>.Instance))
{
}
public CycloneDxExtractor(IParsedSbomParser parser)
{
_parser = parser ?? throw new ArgumentNullException(nameof(parser));
}
/// <inheritdoc />
public ParsedSbom ExtractParsed(JsonDocument document)
{
ArgumentNullException.ThrowIfNull(document);
var payload = Encoding.UTF8.GetBytes(document.RootElement.GetRawText());
using var stream = new MemoryStream(payload);
return _parser.ParseAsync(stream, SbomFormat.CycloneDX).GetAwaiter().GetResult();
}
/// <inheritdoc />
public async Task<ParsedSbom> ExtractParsedAsync(Stream stream, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(stream);
if (stream.CanSeek)
{
stream.Position = 0;
}
return await _parser.ParseAsync(stream, SbomFormat.CycloneDX, ct);
}
/// <inheritdoc />
public CycloneDxMetadata Extract(JsonDocument document)
{
@@ -103,62 +153,15 @@ public sealed class CycloneDxExtractor : ICycloneDxExtractor
try
{
var root = document.RootElement;
var parsed = ExtractParsed(document);
var version = ExtractBomVersion(root);
// Extract serial number
string? serialNumber = null;
if (root.TryGetProperty("serialNumber", out var serialProp))
{
serialNumber = serialProp.GetString();
}
// Extract version
int version = 1;
if (root.TryGetProperty("version", out var versionProp))
{
version = versionProp.GetInt32();
}
// Extract spec version
string? specVersion = null;
if (root.TryGetProperty("specVersion", out var specProp))
{
specVersion = specProp.GetString();
}
// Extract primary component from metadata
string? primaryBomRef = null;
string? primaryName = null;
string? primaryVersion = null;
string? primaryPurl = null;
if (root.TryGetProperty("metadata", out var metadata))
{
if (metadata.TryGetProperty("component", out var primaryComponent))
{
primaryBomRef = GetStringProperty(primaryComponent, "bom-ref");
primaryName = GetStringProperty(primaryComponent, "name");
primaryVersion = GetStringProperty(primaryComponent, "version");
primaryPurl = GetStringProperty(primaryComponent, "purl");
}
}
// Extract timestamp
DateTimeOffset? timestamp = null;
if (root.TryGetProperty("metadata", out var meta2) &&
meta2.TryGetProperty("timestamp", out var tsProp))
{
if (DateTimeOffset.TryParse(tsProp.GetString(), out var ts))
{
timestamp = ts;
}
}
// Extract all component bom-refs and purls
var bomRefs = new List<string>();
var purls = new List<string>();
int componentCount = 0;
if (root.TryGetProperty("components", out var components))
if (root.TryGetProperty("components", out var components) &&
components.ValueKind == JsonValueKind.Array)
{
foreach (var component in components.EnumerateArray())
{
@@ -181,19 +184,23 @@ public sealed class CycloneDxExtractor : ICycloneDxExtractor
}
}
var primaryComponent = GetPrimaryComponent(parsed);
return new CycloneDxMetadata
{
SerialNumber = serialNumber,
SerialNumber = NormalizeParsedString(parsed.SerialNumber),
Version = version,
SpecVersion = specVersion,
PrimaryBomRef = primaryBomRef,
PrimaryName = primaryName,
PrimaryVersion = primaryVersion,
PrimaryPurl = primaryPurl,
SpecVersion = NormalizeParsedString(parsed.SpecVersion),
PrimaryBomRef = NormalizeParsedString(parsed.Metadata.RootComponentRef),
PrimaryName = NormalizeParsedString(parsed.Metadata.Name)
?? NormalizeParsedString(primaryComponent?.Name),
PrimaryVersion = NormalizeParsedString(parsed.Metadata.Version)
?? NormalizeParsedString(primaryComponent?.Version),
PrimaryPurl = NormalizeParsedString(primaryComponent?.Purl),
ComponentBomRefs = bomRefs,
ComponentPurls = purls,
ComponentCount = componentCount,
Timestamp = timestamp,
Timestamp = parsed.Metadata.Timestamp,
Success = true
};
}
@@ -410,13 +417,13 @@ public sealed class CycloneDxExtractor : ICycloneDxExtractor
if (firstChar == '\uFEFF')
{
buffer = new byte[1];
await stream.ReadAsync(buffer, ct);
await stream.ReadExactlyAsync(buffer, ct);
stream.Position = 0;
// Skip 3-byte UTF-8 BOM
if (stream.Length >= 3)
{
var bomBuffer = new byte[3];
await stream.ReadAsync(bomBuffer, ct);
await stream.ReadExactlyAsync(bomBuffer, ct);
if (bomBuffer[0] == 0xEF && bomBuffer[1] == 0xBB && bomBuffer[2] == 0xBF)
{
firstChar = (char)stream.ReadByte();
@@ -483,6 +490,34 @@ public sealed class CycloneDxExtractor : ICycloneDxExtractor
return null;
}
private static int ExtractBomVersion(JsonElement root)
{
if (root.TryGetProperty("version", out var versionProp) &&
versionProp.ValueKind == JsonValueKind.Number &&
versionProp.TryGetInt32(out var version))
{
return version;
}
return 1;
}
private static ParsedComponent? GetPrimaryComponent(ParsedSbom parsed)
{
if (string.IsNullOrWhiteSpace(parsed.Metadata.RootComponentRef))
{
return null;
}
return parsed.Components.FirstOrDefault(component =>
string.Equals(component.BomRef, parsed.Metadata.RootComponentRef, StringComparison.Ordinal));
}
private static string? NormalizeParsedString(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value;
}
private static void ExtractXmlComponents(
XElement componentsElement,
XNamespace ns,

View File

@@ -12,8 +12,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.SbomIntegration\StellaOps.Concelier.SbomIntegration.csproj" />
</ItemGroup>
</Project>