license switch agpl -> busl1, sprints work, new product advisories
This commit is contained in:
17
src/__Libraries/StellaOps.Artifact.Core/AGENTS.md
Normal file
17
src/__Libraries/StellaOps.Artifact.Core/AGENTS.md
Normal 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.
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user