audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
25
src/__Libraries/StellaOps.Spdx3/AGENTS.md
Normal file
25
src/__Libraries/StellaOps.Spdx3/AGENTS.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# SPDX3 Library Charter
|
||||
|
||||
## Mission
|
||||
- Provide SPDX 3.0.1 parsing, validation, and profile support.
|
||||
|
||||
## Responsibilities
|
||||
- Parse SPDX JSON-LD and surface deterministic models.
|
||||
- Validate profile conformance and identifiers.
|
||||
- Resolve contexts with offline-friendly defaults.
|
||||
|
||||
## Required Reading
|
||||
- docs/README.md
|
||||
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
|
||||
- docs/modules/platform/architecture-overview.md
|
||||
- docs/modules/sbom-service/architecture.md
|
||||
- docs/modules/sbom-service/spdx3-profile-support.md
|
||||
|
||||
## Working Agreement
|
||||
- Deterministic parsing and invariant formatting.
|
||||
- Use TimeProvider and IGuidGenerator where timestamps or IDs are created.
|
||||
- Avoid network dependencies unless explicitly enabled.
|
||||
|
||||
## Testing Strategy
|
||||
- Unit tests for parser/validator behavior and error paths.
|
||||
- Determinism tests for stable ordering and output.
|
||||
142
src/__Libraries/StellaOps.Spdx3/ISpdx3Parser.cs
Normal file
142
src/__Libraries/StellaOps.Spdx3/ISpdx3Parser.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
// <copyright file="ISpdx3Parser.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using StellaOps.Spdx3.Model;
|
||||
|
||||
namespace StellaOps.Spdx3;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for SPDX 3.0.1 document parsing.
|
||||
/// </summary>
|
||||
public interface ISpdx3Parser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses an SPDX 3.0.1 document from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The input stream containing JSON-LD.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The parse result.</returns>
|
||||
Task<Spdx3ParseResult> ParseAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Parses an SPDX 3.0.1 document from a file path.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the JSON-LD file.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The parse result.</returns>
|
||||
Task<Spdx3ParseResult> ParseAsync(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Parses an SPDX 3.0.1 document from JSON text.
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON-LD text.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The parse result.</returns>
|
||||
Task<Spdx3ParseResult> ParseFromJsonAsync(
|
||||
string json,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of parsing an SPDX 3.0.1 document.
|
||||
/// </summary>
|
||||
public sealed record Spdx3ParseResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether parsing was successful.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parsed document (if successful).
|
||||
/// </summary>
|
||||
public Spdx3Document? Document { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets any parsing errors.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Spdx3ParseError> Errors { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets any parsing warnings.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Spdx3ParseWarning> Warnings { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful parse result.
|
||||
/// </summary>
|
||||
/// <param name="document">The parsed document.</param>
|
||||
/// <param name="warnings">Any warnings.</param>
|
||||
/// <returns>The result.</returns>
|
||||
public static Spdx3ParseResult Succeeded(
|
||||
Spdx3Document document,
|
||||
IReadOnlyList<Spdx3ParseWarning>? warnings = null)
|
||||
{
|
||||
return new Spdx3ParseResult
|
||||
{
|
||||
Success = true,
|
||||
Document = document,
|
||||
Warnings = warnings ?? []
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed parse result.
|
||||
/// </summary>
|
||||
/// <param name="errors">The errors.</param>
|
||||
/// <param name="warnings">Any warnings.</param>
|
||||
/// <returns>The result.</returns>
|
||||
public static Spdx3ParseResult Failed(
|
||||
IReadOnlyList<Spdx3ParseError> errors,
|
||||
IReadOnlyList<Spdx3ParseWarning>? warnings = null)
|
||||
{
|
||||
return new Spdx3ParseResult
|
||||
{
|
||||
Success = false,
|
||||
Errors = errors,
|
||||
Warnings = warnings ?? []
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed parse result from a single error.
|
||||
/// </summary>
|
||||
/// <param name="code">The error code.</param>
|
||||
/// <param name="message">The error message.</param>
|
||||
/// <param name="path">The JSON path where the error occurred.</param>
|
||||
/// <returns>The result.</returns>
|
||||
public static Spdx3ParseResult Failed(
|
||||
string code,
|
||||
string message,
|
||||
string? path = null)
|
||||
{
|
||||
return Failed([new Spdx3ParseError(code, message, path)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a parsing error.
|
||||
/// </summary>
|
||||
/// <param name="Code">Error code.</param>
|
||||
/// <param name="Message">Error message.</param>
|
||||
/// <param name="Path">JSON path where the error occurred.</param>
|
||||
public sealed record Spdx3ParseError(
|
||||
string Code,
|
||||
string Message,
|
||||
string? Path = null);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a parsing warning.
|
||||
/// </summary>
|
||||
/// <param name="Code">Warning code.</param>
|
||||
/// <param name="Message">Warning message.</param>
|
||||
/// <param name="Path">JSON path where the warning occurred.</param>
|
||||
public sealed record Spdx3ParseWarning(
|
||||
string Code,
|
||||
string Message,
|
||||
string? Path = null);
|
||||
247
src/__Libraries/StellaOps.Spdx3/JsonLd/Spdx3ContextResolver.cs
Normal file
247
src/__Libraries/StellaOps.Spdx3/JsonLd/Spdx3ContextResolver.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
// <copyright file="Spdx3ContextResolver.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Spdx3.JsonLd;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves JSON-LD contexts for SPDX 3.0.1 documents.
|
||||
/// </summary>
|
||||
public interface ISpdx3ContextResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a context from a URL or embedded reference.
|
||||
/// </summary>
|
||||
/// <param name="contextRef">The context reference (URL or inline).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The resolved context as a JSON element.</returns>
|
||||
Task<Spdx3Context?> ResolveAsync(
|
||||
string contextRef,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a resolved JSON-LD context.
|
||||
/// </summary>
|
||||
public sealed record Spdx3Context
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the context URI.
|
||||
/// </summary>
|
||||
public required string Uri { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the context document.
|
||||
/// </summary>
|
||||
public required JsonDocument Document { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets when the context was resolved.
|
||||
/// </summary>
|
||||
public required DateTimeOffset ResolvedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for the SPDX 3 context resolver.
|
||||
/// </summary>
|
||||
public sealed class Spdx3ContextResolverOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the cache TTL for resolved contexts.
|
||||
/// </summary>
|
||||
public TimeSpan CacheTtl { get; set; } = TimeSpan.FromHours(24);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum cache size.
|
||||
/// </summary>
|
||||
public int MaxCacheSize { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to allow remote context resolution.
|
||||
/// Set to false for air-gapped environments.
|
||||
/// </summary>
|
||||
public bool AllowRemoteContexts { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the base path for local context files.
|
||||
/// </summary>
|
||||
public string? LocalContextPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP timeout for remote contexts.
|
||||
/// </summary>
|
||||
public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of context resolver with caching.
|
||||
/// </summary>
|
||||
public sealed class Spdx3ContextResolver : ISpdx3ContextResolver, IDisposable
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ILogger<Spdx3ContextResolver> _logger;
|
||||
private readonly Spdx3ContextResolverOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly Dictionary<string, string> EmbeddedContexts = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["https://spdx.org/rdf/3.0.1/spdx-context.jsonld"] = GetEmbeddedContext("spdx-context.jsonld"),
|
||||
["https://spdx.org/rdf/3.0.1/terms/Core"] = GetEmbeddedContext("core-profile.jsonld"),
|
||||
["https://spdx.org/rdf/3.0.1/terms/Software"] = GetEmbeddedContext("software-profile.jsonld"),
|
||||
["https://spdx.org/rdf/3.0.1/terms/Security"] = GetEmbeddedContext("security-profile.jsonld"),
|
||||
["https://spdx.org/rdf/3.0.1/terms/Build"] = GetEmbeddedContext("build-profile.jsonld"),
|
||||
["https://spdx.org/rdf/3.0.1/terms/Lite"] = GetEmbeddedContext("lite-profile.jsonld")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Spdx3ContextResolver"/> class.
|
||||
/// </summary>
|
||||
public Spdx3ContextResolver(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IMemoryCache cache,
|
||||
ILogger<Spdx3ContextResolver> logger,
|
||||
IOptions<Spdx3ContextResolverOptions> options,
|
||||
TimeProvider timeProvider)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Spdx3Context?> ResolveAsync(
|
||||
string contextRef,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contextRef))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
var cacheKey = $"spdx3-context:{contextRef}";
|
||||
if (_cache.TryGetValue(cacheKey, out Spdx3Context? cached))
|
||||
{
|
||||
_logger.LogDebug("Context cache hit for {ContextRef}", contextRef);
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Try embedded contexts first (for air-gap support)
|
||||
if (EmbeddedContexts.TryGetValue(contextRef, out var embedded))
|
||||
{
|
||||
var context = CreateContext(contextRef, embedded);
|
||||
CacheContext(cacheKey, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
// Try local file if configured
|
||||
if (!string.IsNullOrEmpty(_options.LocalContextPath))
|
||||
{
|
||||
var localPath = Path.Combine(_options.LocalContextPath, GetContextFileName(contextRef));
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(localPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var context = CreateContext(contextRef, content);
|
||||
CacheContext(cacheKey, context);
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch remote if allowed
|
||||
if (!_options.AllowRemoteContexts)
|
||||
{
|
||||
_logger.LogWarning("Remote context resolution disabled, cannot resolve {ContextRef}", contextRef);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await FetchRemoteContextAsync(contextRef, cacheKey, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<Spdx3Context?> FetchRemoteContextAsync(
|
||||
string contextRef,
|
||||
string cacheKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("Spdx3Context");
|
||||
client.Timeout = _options.HttpTimeout;
|
||||
|
||||
_logger.LogInformation("Fetching remote context {ContextRef}", contextRef);
|
||||
var content = await client.GetStringAsync(contextRef, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var context = CreateContext(contextRef, content);
|
||||
CacheContext(cacheKey, context);
|
||||
return context;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch remote context {ContextRef}", contextRef);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Spdx3Context CreateContext(string uri, string content)
|
||||
{
|
||||
return new Spdx3Context
|
||||
{
|
||||
Uri = uri,
|
||||
Document = JsonDocument.Parse(content),
|
||||
ResolvedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
private void CacheContext(string cacheKey, Spdx3Context context)
|
||||
{
|
||||
var cacheOptions = new MemoryCacheEntryOptions
|
||||
{
|
||||
Size = 1,
|
||||
SlidingExpiration = _options.CacheTtl
|
||||
};
|
||||
_cache.Set(cacheKey, context, cacheOptions);
|
||||
}
|
||||
|
||||
private static string GetContextFileName(string uri)
|
||||
{
|
||||
var lastSlash = uri.LastIndexOf('/');
|
||||
return lastSlash >= 0 ? uri[(lastSlash + 1)..] : uri;
|
||||
}
|
||||
|
||||
private static string GetEmbeddedContext(string name)
|
||||
{
|
||||
// In a real implementation, this would load from embedded resources
|
||||
// For now, return a minimal stub that allows parsing
|
||||
return name switch
|
||||
{
|
||||
"spdx-context.jsonld" => """
|
||||
{
|
||||
"@context": {
|
||||
"spdx": "https://spdx.org/rdf/3.0.1/terms/",
|
||||
"Core": "spdx:Core/",
|
||||
"Software": "spdx:Software/",
|
||||
"spdxId": "@id",
|
||||
"@type": "@type"
|
||||
}
|
||||
}
|
||||
""",
|
||||
_ => "{}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
// IMemoryCache is typically managed by DI
|
||||
}
|
||||
}
|
||||
319
src/__Libraries/StellaOps.Spdx3/Model/Software/Spdx3Package.cs
Normal file
319
src/__Libraries/StellaOps.Spdx3/Model/Software/Spdx3Package.cs
Normal file
@@ -0,0 +1,319 @@
|
||||
// <copyright file="Spdx3Package.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Spdx3.Model.Software;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an SPDX 3.0.1 Package (Software Profile).
|
||||
/// </summary>
|
||||
public sealed record Spdx3Package : Spdx3Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the package version.
|
||||
/// </summary>
|
||||
[JsonPropertyName("packageVersion")]
|
||||
public string? PackageVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the download location URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("downloadLocation")]
|
||||
public string? DownloadLocation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Package URL (PURL).
|
||||
/// </summary>
|
||||
[JsonPropertyName("packageUrl")]
|
||||
public string? PackageUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the home page URL.
|
||||
/// </summary>
|
||||
[JsonPropertyName("homePage")]
|
||||
public string? HomePage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets source information.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sourceInfo")]
|
||||
public string? SourceInfo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary purpose of the package.
|
||||
/// </summary>
|
||||
[JsonPropertyName("primaryPurpose")]
|
||||
public Spdx3SoftwarePurpose? PrimaryPurpose { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets additional purposes.
|
||||
/// </summary>
|
||||
[JsonPropertyName("additionalPurpose")]
|
||||
public ImmutableArray<Spdx3SoftwarePurpose> AdditionalPurpose { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the copyright text.
|
||||
/// </summary>
|
||||
[JsonPropertyName("copyrightText")]
|
||||
public string? CopyrightText { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets attribution text.
|
||||
/// </summary>
|
||||
[JsonPropertyName("attributionText")]
|
||||
public ImmutableArray<string> AttributionText { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the originator of the package.
|
||||
/// </summary>
|
||||
[JsonPropertyName("originatedBy")]
|
||||
public ImmutableArray<string> OriginatedBy { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supplier of the package.
|
||||
/// </summary>
|
||||
[JsonPropertyName("suppliedBy")]
|
||||
public string? SuppliedBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the release time of the package.
|
||||
/// </summary>
|
||||
[JsonPropertyName("releaseTime")]
|
||||
public DateTimeOffset? ReleaseTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the build time of the package.
|
||||
/// </summary>
|
||||
[JsonPropertyName("buildTime")]
|
||||
public DateTimeOffset? BuildTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the valid until time.
|
||||
/// </summary>
|
||||
[JsonPropertyName("validUntilTime")]
|
||||
public DateTimeOffset? ValidUntilTime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an SPDX 3.0.1 File (Software Profile).
|
||||
/// </summary>
|
||||
public sealed record Spdx3File : Spdx3Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the file name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public new required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary purpose.
|
||||
/// </summary>
|
||||
[JsonPropertyName("primaryPurpose")]
|
||||
public Spdx3SoftwarePurpose? PrimaryPurpose { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content type (MIME type).
|
||||
/// </summary>
|
||||
[JsonPropertyName("contentType")]
|
||||
public string? ContentType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the copyright text.
|
||||
/// </summary>
|
||||
[JsonPropertyName("copyrightText")]
|
||||
public string? CopyrightText { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an SPDX 3.0.1 Snippet (Software Profile).
|
||||
/// </summary>
|
||||
public sealed record Spdx3Snippet : Spdx3Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the file containing this snippet.
|
||||
/// </summary>
|
||||
[JsonPropertyName("snippetFromFile")]
|
||||
public required string SnippetFromFile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the byte range.
|
||||
/// </summary>
|
||||
[JsonPropertyName("byteRange")]
|
||||
public Spdx3PositiveIntegerRange? ByteRange { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the line range.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineRange")]
|
||||
public Spdx3PositiveIntegerRange? LineRange { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary purpose.
|
||||
/// </summary>
|
||||
[JsonPropertyName("primaryPurpose")]
|
||||
public Spdx3SoftwarePurpose? PrimaryPurpose { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the copyright text.
|
||||
/// </summary>
|
||||
[JsonPropertyName("copyrightText")]
|
||||
public string? CopyrightText { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a positive integer range.
|
||||
/// </summary>
|
||||
public sealed record Spdx3PositiveIntegerRange
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the begin value.
|
||||
/// </summary>
|
||||
[JsonPropertyName("beginIntegerRange")]
|
||||
public required int Begin { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end value.
|
||||
/// </summary>
|
||||
[JsonPropertyName("endIntegerRange")]
|
||||
public required int End { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Software purpose types.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum Spdx3SoftwarePurpose
|
||||
{
|
||||
/// <summary>
|
||||
/// Application software.
|
||||
/// </summary>
|
||||
Application,
|
||||
|
||||
/// <summary>
|
||||
/// Archive (zip, tar, etc.).
|
||||
/// </summary>
|
||||
Archive,
|
||||
|
||||
/// <summary>
|
||||
/// BOM (Bill of Materials).
|
||||
/// </summary>
|
||||
Bom,
|
||||
|
||||
/// <summary>
|
||||
/// Configuration file.
|
||||
/// </summary>
|
||||
Configuration,
|
||||
|
||||
/// <summary>
|
||||
/// Container image.
|
||||
/// </summary>
|
||||
Container,
|
||||
|
||||
/// <summary>
|
||||
/// Data file.
|
||||
/// </summary>
|
||||
Data,
|
||||
|
||||
/// <summary>
|
||||
/// Device driver.
|
||||
/// </summary>
|
||||
Device,
|
||||
|
||||
/// <summary>
|
||||
/// Device driver (alternative).
|
||||
/// </summary>
|
||||
DeviceDriver,
|
||||
|
||||
/// <summary>
|
||||
/// Documentation.
|
||||
/// </summary>
|
||||
Documentation,
|
||||
|
||||
/// <summary>
|
||||
/// Evidence (compliance).
|
||||
/// </summary>
|
||||
Evidence,
|
||||
|
||||
/// <summary>
|
||||
/// Executable.
|
||||
/// </summary>
|
||||
Executable,
|
||||
|
||||
/// <summary>
|
||||
/// File.
|
||||
/// </summary>
|
||||
File,
|
||||
|
||||
/// <summary>
|
||||
/// Firmware.
|
||||
/// </summary>
|
||||
Firmware,
|
||||
|
||||
/// <summary>
|
||||
/// Framework.
|
||||
/// </summary>
|
||||
Framework,
|
||||
|
||||
/// <summary>
|
||||
/// Install script.
|
||||
/// </summary>
|
||||
Install,
|
||||
|
||||
/// <summary>
|
||||
/// Library.
|
||||
/// </summary>
|
||||
Library,
|
||||
|
||||
/// <summary>
|
||||
/// Machine learning model.
|
||||
/// </summary>
|
||||
Model,
|
||||
|
||||
/// <summary>
|
||||
/// Module.
|
||||
/// </summary>
|
||||
Module,
|
||||
|
||||
/// <summary>
|
||||
/// Operating system.
|
||||
/// </summary>
|
||||
OperatingSystem,
|
||||
|
||||
/// <summary>
|
||||
/// Other purpose.
|
||||
/// </summary>
|
||||
Other,
|
||||
|
||||
/// <summary>
|
||||
/// Patch.
|
||||
/// </summary>
|
||||
Patch,
|
||||
|
||||
/// <summary>
|
||||
/// Platform.
|
||||
/// </summary>
|
||||
Platform,
|
||||
|
||||
/// <summary>
|
||||
/// Requirement.
|
||||
/// </summary>
|
||||
Requirement,
|
||||
|
||||
/// <summary>
|
||||
/// Source code.
|
||||
/// </summary>
|
||||
Source,
|
||||
|
||||
/// <summary>
|
||||
/// Specification.
|
||||
/// </summary>
|
||||
Specification,
|
||||
|
||||
/// <summary>
|
||||
/// Test.
|
||||
/// </summary>
|
||||
Test
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// <copyright file="Spdx3SpdxDocument.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Spdx3.Model.Software;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an SPDX 3.0.1 SpdxDocument (Software Profile).
|
||||
/// The SpdxDocument is the root element that describes the SBOM itself.
|
||||
/// </summary>
|
||||
public sealed record Spdx3SpdxDocument : Spdx3Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the namespace for this document.
|
||||
/// </summary>
|
||||
[JsonPropertyName("namespaceMap")]
|
||||
public ImmutableArray<Spdx3NamespaceMap> NamespaceMap { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the elements described by this document.
|
||||
/// </summary>
|
||||
[JsonPropertyName("element")]
|
||||
public ImmutableArray<string> Element { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root elements of this document.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rootElement")]
|
||||
public ImmutableArray<string> RootElement { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the imports (external document references).
|
||||
/// </summary>
|
||||
[JsonPropertyName("import")]
|
||||
public ImmutableArray<Spdx3ExternalMap> Import { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a namespace mapping.
|
||||
/// </summary>
|
||||
public sealed record Spdx3NamespaceMap
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the prefix.
|
||||
/// </summary>
|
||||
[JsonPropertyName("prefix")]
|
||||
public required string Prefix { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the namespace URI.
|
||||
/// </summary>
|
||||
[JsonPropertyName("namespace")]
|
||||
public required string Namespace { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an external map (import).
|
||||
/// </summary>
|
||||
public sealed record Spdx3ExternalMap
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the external SPDX ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("externalSpdxId")]
|
||||
public required string ExternalSpdxId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the verified using integrity methods.
|
||||
/// </summary>
|
||||
[JsonPropertyName("verifiedUsing")]
|
||||
public ImmutableArray<Spdx3IntegrityMethod> VerifiedUsing { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the location hint.
|
||||
/// </summary>
|
||||
[JsonPropertyName("locationHint")]
|
||||
public string? LocationHint { get; init; }
|
||||
}
|
||||
139
src/__Libraries/StellaOps.Spdx3/Model/Spdx3CreationInfo.cs
Normal file
139
src/__Libraries/StellaOps.Spdx3/Model/Spdx3CreationInfo.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
// <copyright file="Spdx3CreationInfo.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Spdx3.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the creation information for SPDX 3.0.1 elements.
|
||||
/// This captures who created the document, when, and with what tools.
|
||||
/// </summary>
|
||||
public sealed record Spdx3CreationInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique identifier for this CreationInfo.
|
||||
/// </summary>
|
||||
[JsonPropertyName("@id")]
|
||||
public string? Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("@type")]
|
||||
public string? Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SPDX specification version.
|
||||
/// Must be "3.0.1" for SPDX 3.0.1 documents.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("specVersion")]
|
||||
public required string SpecVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the creation timestamp in ISO 8601 format.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("created")]
|
||||
public required DateTimeOffset Created { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agents (persons/organizations) who created this.
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdBy")]
|
||||
public ImmutableArray<string> CreatedBy { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tools used to create this.
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdUsing")]
|
||||
public ImmutableArray<string> CreatedUsing { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the profiles this document conforms to.
|
||||
/// </summary>
|
||||
[JsonPropertyName("profile")]
|
||||
public ImmutableArray<Spdx3ProfileIdentifier> Profile { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data license.
|
||||
/// Must be CC0-1.0 for SPDX documents.
|
||||
/// </summary>
|
||||
[JsonPropertyName("dataLicense")]
|
||||
public string? DataLicense { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an optional comment about the creation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("comment")]
|
||||
public string? Comment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The standard SPDX 3.0.1 spec version string.
|
||||
/// </summary>
|
||||
public const string Spdx301Version = "3.0.1";
|
||||
|
||||
/// <summary>
|
||||
/// The required data license for SPDX documents.
|
||||
/// </summary>
|
||||
public const string Spdx301DataLicense = "CC0-1.0";
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the spec version is 3.0.1.
|
||||
/// </summary>
|
||||
/// <returns>True if valid SPDX 3.0.1 spec version.</returns>
|
||||
public bool IsValidSpecVersion() =>
|
||||
string.Equals(SpecVersion, Spdx301Version, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Agent (person, organization, or software agent).
|
||||
/// </summary>
|
||||
public sealed record Spdx3Agent : Spdx3Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the agent's name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public new required string Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Person agent.
|
||||
/// </summary>
|
||||
public sealed record Spdx3Person : Spdx3Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the person's name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public new required string Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Organization agent.
|
||||
/// </summary>
|
||||
public sealed record Spdx3Organization : Spdx3Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the organization's name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public new required string Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Tool agent.
|
||||
/// </summary>
|
||||
public sealed record Spdx3Tool : Spdx3Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the tool's name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public new required string Name { get; init; }
|
||||
}
|
||||
218
src/__Libraries/StellaOps.Spdx3/Model/Spdx3Document.cs
Normal file
218
src/__Libraries/StellaOps.Spdx3/Model/Spdx3Document.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
// <copyright file="Spdx3Document.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
|
||||
namespace StellaOps.Spdx3.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a parsed SPDX 3.0.1 document containing all elements.
|
||||
/// </summary>
|
||||
public sealed class Spdx3Document
|
||||
{
|
||||
private readonly Dictionary<string, Spdx3Element> _elementsById;
|
||||
private readonly Dictionary<string, Spdx3CreationInfo> _creationInfoById;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Spdx3Document"/> class.
|
||||
/// </summary>
|
||||
/// <param name="elements">All elements in the document.</param>
|
||||
/// <param name="creationInfos">All CreationInfo objects.</param>
|
||||
/// <param name="profiles">Detected profile conformance.</param>
|
||||
/// <param name="spdxDocument">The root SpdxDocument element if present.</param>
|
||||
public Spdx3Document(
|
||||
IEnumerable<Spdx3Element> elements,
|
||||
IEnumerable<Spdx3CreationInfo> creationInfos,
|
||||
IEnumerable<Spdx3ProfileIdentifier> profiles,
|
||||
Spdx3SpdxDocument? spdxDocument = null)
|
||||
{
|
||||
var elementList = elements.ToList();
|
||||
|
||||
// Use GroupBy to handle duplicates - last element wins for lookup but keeps all for validation
|
||||
_elementsById = elementList
|
||||
.GroupBy(e => e.SpdxId, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.Last(), StringComparer.Ordinal);
|
||||
|
||||
_creationInfoById = creationInfos
|
||||
.Where(c => c.Id != null)
|
||||
.GroupBy(c => c.Id!, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.Last(), StringComparer.Ordinal);
|
||||
|
||||
Profiles = profiles.ToImmutableHashSet();
|
||||
SpdxDocument = spdxDocument;
|
||||
|
||||
// Categorize elements by type - use original list to preserve duplicates for counting
|
||||
Packages = elementList.OfType<Spdx3Package>().ToImmutableArray();
|
||||
Files = elementList.OfType<Spdx3File>().ToImmutableArray();
|
||||
Snippets = elementList.OfType<Spdx3Snippet>().ToImmutableArray();
|
||||
Relationships = elementList.OfType<Spdx3Relationship>().ToImmutableArray();
|
||||
AllElements = elementList.ToImmutableArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all elements including duplicates (for validation).
|
||||
/// </summary>
|
||||
public ImmutableArray<Spdx3Element> AllElements { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root SpdxDocument element if present.
|
||||
/// </summary>
|
||||
public Spdx3SpdxDocument? SpdxDocument { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all elements in the document.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Spdx3Element> Elements => _elementsById.Values;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all packages in the document.
|
||||
/// </summary>
|
||||
public ImmutableArray<Spdx3Package> Packages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all files in the document.
|
||||
/// </summary>
|
||||
public ImmutableArray<Spdx3File> Files { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all snippets in the document.
|
||||
/// </summary>
|
||||
public ImmutableArray<Spdx3Snippet> Snippets { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all relationships in the document.
|
||||
/// </summary>
|
||||
public ImmutableArray<Spdx3Relationship> Relationships { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the detected profile conformance.
|
||||
/// </summary>
|
||||
public ImmutableHashSet<Spdx3ProfileIdentifier> Profiles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an element by its SPDX ID.
|
||||
/// </summary>
|
||||
/// <param name="spdxId">The SPDX ID.</param>
|
||||
/// <returns>The element, or null if not found.</returns>
|
||||
public Spdx3Element? GetById(string spdxId)
|
||||
{
|
||||
return _elementsById.TryGetValue(spdxId, out var element) ? element : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an element by its SPDX ID as a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The element type.</typeparam>
|
||||
/// <param name="spdxId">The SPDX ID.</param>
|
||||
/// <returns>The element, or null if not found or wrong type.</returns>
|
||||
public T? GetById<T>(string spdxId) where T : Spdx3Element
|
||||
{
|
||||
return GetById(spdxId) as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a CreationInfo by its ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The CreationInfo ID.</param>
|
||||
/// <returns>The CreationInfo, or null if not found.</returns>
|
||||
public Spdx3CreationInfo? GetCreationInfo(string id)
|
||||
{
|
||||
return _creationInfoById.TryGetValue(id, out var info) ? info : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets relationships where the given element is the source.
|
||||
/// </summary>
|
||||
/// <param name="spdxId">The source element ID.</param>
|
||||
/// <returns>Matching relationships.</returns>
|
||||
public IEnumerable<Spdx3Relationship> GetRelationshipsFrom(string spdxId)
|
||||
{
|
||||
return Relationships.Where(r => r.From == spdxId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets relationships where the given element is a target.
|
||||
/// </summary>
|
||||
/// <param name="spdxId">The target element ID.</param>
|
||||
/// <returns>Matching relationships.</returns>
|
||||
public IEnumerable<Spdx3Relationship> GetRelationshipsTo(string spdxId)
|
||||
{
|
||||
return Relationships.Where(r => r.To.Contains(spdxId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets direct dependencies of a package.
|
||||
/// </summary>
|
||||
/// <param name="packageId">The package SPDX ID.</param>
|
||||
/// <returns>Dependent packages.</returns>
|
||||
public IEnumerable<Spdx3Package> GetDependencies(string packageId)
|
||||
{
|
||||
return GetRelationshipsFrom(packageId)
|
||||
.Where(r => r.RelationshipType == Spdx3RelationshipType.DependsOn)
|
||||
.SelectMany(r => r.To)
|
||||
.Select(GetById<Spdx3Package>)
|
||||
.Where(p => p != null)
|
||||
.Cast<Spdx3Package>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all files contained in a package.
|
||||
/// </summary>
|
||||
/// <param name="packageId">The package SPDX ID.</param>
|
||||
/// <returns>Contained files.</returns>
|
||||
public IEnumerable<Spdx3File> GetContainedFiles(string packageId)
|
||||
{
|
||||
return GetRelationshipsFrom(packageId)
|
||||
.Where(r => r.RelationshipType == Spdx3RelationshipType.Contains)
|
||||
.SelectMany(r => r.To)
|
||||
.Select(GetById<Spdx3File>)
|
||||
.Where(f => f != null)
|
||||
.Cast<Spdx3File>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the document conforms to a specific profile.
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile to check.</param>
|
||||
/// <returns>True if the document conforms.</returns>
|
||||
public bool ConformsTo(Spdx3ProfileIdentifier profile)
|
||||
{
|
||||
return Profiles.Contains(profile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all PURLs from packages in the document.
|
||||
/// </summary>
|
||||
/// <returns>Package URLs.</returns>
|
||||
public IEnumerable<string> GetAllPurls()
|
||||
{
|
||||
return Packages
|
||||
.SelectMany(p => p.ExternalIdentifier)
|
||||
.Where(i => i.ExternalIdentifierType == Spdx3ExternalIdentifierType.PackageUrl)
|
||||
.Select(i => i.Identifier)
|
||||
.Distinct(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root package (if any).
|
||||
/// </summary>
|
||||
/// <returns>The root package, or null.</returns>
|
||||
public Spdx3Package? GetRootPackage()
|
||||
{
|
||||
if (SpdxDocument?.RootElement.Length > 0)
|
||||
{
|
||||
var rootId = SpdxDocument.RootElement[0];
|
||||
return GetById<Spdx3Package>(rootId);
|
||||
}
|
||||
|
||||
// Fallback: find package with no incoming Contains relationships
|
||||
var containedIds = Relationships
|
||||
.Where(r => r.RelationshipType == Spdx3RelationshipType.Contains)
|
||||
.SelectMany(r => r.To)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
return Packages.FirstOrDefault(p => !containedIds.Contains(p.SpdxId));
|
||||
}
|
||||
}
|
||||
113
src/__Libraries/StellaOps.Spdx3/Model/Spdx3Element.cs
Normal file
113
src/__Libraries/StellaOps.Spdx3/Model/Spdx3Element.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
// <copyright file="Spdx3Element.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Spdx3.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all SPDX 3.0.1 elements.
|
||||
/// Every element in SPDX 3.0 derives from this abstract type.
|
||||
/// </summary>
|
||||
public abstract record Spdx3Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique IRI identifier for this element.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This must be globally unique and serves as the element's identity.
|
||||
/// Format: URN, URL, or document-scoped ID with #fragment.
|
||||
/// </remarks>
|
||||
[Required]
|
||||
[JsonPropertyName("spdxId")]
|
||||
public required string SpdxId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the element type from JSON-LD.
|
||||
/// </summary>
|
||||
[JsonPropertyName("@type")]
|
||||
public string? Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reference to creation information.
|
||||
/// Used when CreationInfo is shared across elements.
|
||||
/// </summary>
|
||||
[JsonPropertyName("creationInfo")]
|
||||
public string? CreationInfoRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inline creation information.
|
||||
/// Used when CreationInfo is specific to this element.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Spdx3CreationInfo? CreationInfo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the human-readable name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a brief summary (short description).
|
||||
/// </summary>
|
||||
[JsonPropertyName("summary")]
|
||||
public string? Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a detailed description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an optional comment about this element.
|
||||
/// </summary>
|
||||
[JsonPropertyName("comment")]
|
||||
public string? Comment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets integrity verification methods (hashes, signatures).
|
||||
/// </summary>
|
||||
[JsonPropertyName("verifiedUsing")]
|
||||
public ImmutableArray<Spdx3IntegrityMethod> VerifiedUsing { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets external references (security advisories, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("externalRef")]
|
||||
public ImmutableArray<Spdx3ExternalRef> ExternalRef { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets external identifiers (PURL, CPE, SWID, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("externalIdentifier")]
|
||||
public ImmutableArray<Spdx3ExternalIdentifier> ExternalIdentifier { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets profile-specific extensions.
|
||||
/// </summary>
|
||||
[JsonPropertyName("extension")]
|
||||
public ImmutableArray<Spdx3Extension> Extension { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a profile-specific extension.
|
||||
/// </summary>
|
||||
public sealed record Spdx3Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the extension type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("@type")]
|
||||
public string? Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the extension data as raw JSON.
|
||||
/// </summary>
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object?>? ExtensionData { get; init; }
|
||||
}
|
||||
148
src/__Libraries/StellaOps.Spdx3/Model/Spdx3ExternalIdentifier.cs
Normal file
148
src/__Libraries/StellaOps.Spdx3/Model/Spdx3ExternalIdentifier.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
// <copyright file="Spdx3ExternalIdentifier.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Spdx3.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an external identifier in SPDX 3.0.1.
|
||||
/// Used for PURL, CPE, SWID, and other identifiers.
|
||||
/// </summary>
|
||||
public sealed record Spdx3ExternalIdentifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("@type")]
|
||||
public string? Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("externalIdentifierType")]
|
||||
public Spdx3ExternalIdentifierType? ExternalIdentifierType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifier value.
|
||||
/// </summary>
|
||||
[JsonPropertyName("identifier")]
|
||||
public required string Identifier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an optional comment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("comment")]
|
||||
public string? Comment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the identifying organization.
|
||||
/// </summary>
|
||||
[JsonPropertyName("issuingAuthority")]
|
||||
public string? IssuingAuthority { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// External identifier types in SPDX 3.0.1.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum Spdx3ExternalIdentifierType
|
||||
{
|
||||
/// <summary>
|
||||
/// CPE 2.2 identifier.
|
||||
/// </summary>
|
||||
Cpe22,
|
||||
|
||||
/// <summary>
|
||||
/// CPE 2.3 identifier.
|
||||
/// </summary>
|
||||
Cpe23,
|
||||
|
||||
/// <summary>
|
||||
/// CVE identifier.
|
||||
/// </summary>
|
||||
Cve,
|
||||
|
||||
/// <summary>
|
||||
/// Email identifier.
|
||||
/// </summary>
|
||||
Email,
|
||||
|
||||
/// <summary>
|
||||
/// Git object identifier (SHA).
|
||||
/// </summary>
|
||||
GitOid,
|
||||
|
||||
/// <summary>
|
||||
/// Other identifier type.
|
||||
/// </summary>
|
||||
Other,
|
||||
|
||||
/// <summary>
|
||||
/// Package URL (PURL).
|
||||
/// </summary>
|
||||
PackageUrl,
|
||||
|
||||
/// <summary>
|
||||
/// Security advisory identifier.
|
||||
/// </summary>
|
||||
SecurityOther,
|
||||
|
||||
/// <summary>
|
||||
/// SWHID (Software Heritage ID).
|
||||
/// </summary>
|
||||
Swhid,
|
||||
|
||||
/// <summary>
|
||||
/// SWID tag identifier.
|
||||
/// </summary>
|
||||
Swid,
|
||||
|
||||
/// <summary>
|
||||
/// URL identifier.
|
||||
/// </summary>
|
||||
UrlScheme
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for external identifiers.
|
||||
/// </summary>
|
||||
public static class Spdx3ExternalIdentifierExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts PURL from external identifiers.
|
||||
/// </summary>
|
||||
/// <param name="identifiers">The identifiers to search.</param>
|
||||
/// <returns>The PURL if found, otherwise null.</returns>
|
||||
public static string? GetPurl(this IEnumerable<Spdx3ExternalIdentifier> identifiers)
|
||||
{
|
||||
return identifiers
|
||||
.FirstOrDefault(i => i.ExternalIdentifierType == Spdx3ExternalIdentifierType.PackageUrl)
|
||||
?.Identifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts CPE 2.3 from external identifiers.
|
||||
/// </summary>
|
||||
/// <param name="identifiers">The identifiers to search.</param>
|
||||
/// <returns>The CPE 2.3 if found, otherwise null.</returns>
|
||||
public static string? GetCpe23(this IEnumerable<Spdx3ExternalIdentifier> identifiers)
|
||||
{
|
||||
return identifiers
|
||||
.FirstOrDefault(i => i.ExternalIdentifierType == Spdx3ExternalIdentifierType.Cpe23)
|
||||
?.Identifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts SWHID from external identifiers.
|
||||
/// </summary>
|
||||
/// <param name="identifiers">The identifiers to search.</param>
|
||||
/// <returns>The SWHID if found, otherwise null.</returns>
|
||||
public static string? GetSwhid(this IEnumerable<Spdx3ExternalIdentifier> identifiers)
|
||||
{
|
||||
return identifiers
|
||||
.FirstOrDefault(i => i.ExternalIdentifierType == Spdx3ExternalIdentifierType.Swhid)
|
||||
?.Identifier;
|
||||
}
|
||||
}
|
||||
291
src/__Libraries/StellaOps.Spdx3/Model/Spdx3ExternalRef.cs
Normal file
291
src/__Libraries/StellaOps.Spdx3/Model/Spdx3ExternalRef.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
// <copyright file="Spdx3ExternalRef.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Spdx3.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an external reference in SPDX 3.0.1.
|
||||
/// </summary>
|
||||
public sealed record Spdx3ExternalRef
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("@type")]
|
||||
public string? Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the external reference type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("externalRefType")]
|
||||
public Spdx3ExternalRefType? ExternalRefType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the locator (URI or identifier).
|
||||
/// </summary>
|
||||
[JsonPropertyName("locator")]
|
||||
public ImmutableArray<string> Locator { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content type (MIME type).
|
||||
/// </summary>
|
||||
[JsonPropertyName("contentType")]
|
||||
public string? ContentType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an optional comment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("comment")]
|
||||
public string? Comment { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// External reference types in SPDX 3.0.1.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum Spdx3ExternalRefType
|
||||
{
|
||||
/// <summary>
|
||||
/// Alternate web page.
|
||||
/// </summary>
|
||||
AltWebPage,
|
||||
|
||||
/// <summary>
|
||||
/// Alternate download location.
|
||||
/// </summary>
|
||||
AltDownloadLocation,
|
||||
|
||||
/// <summary>
|
||||
/// Binary artifact.
|
||||
/// </summary>
|
||||
BinaryArtifact,
|
||||
|
||||
/// <summary>
|
||||
/// Bower package reference.
|
||||
/// </summary>
|
||||
Bower,
|
||||
|
||||
/// <summary>
|
||||
/// Build metadata.
|
||||
/// </summary>
|
||||
BuildMeta,
|
||||
|
||||
/// <summary>
|
||||
/// Build system reference.
|
||||
/// </summary>
|
||||
BuildSystem,
|
||||
|
||||
/// <summary>
|
||||
/// Certification reference.
|
||||
/// </summary>
|
||||
Certification,
|
||||
|
||||
/// <summary>
|
||||
/// Chat reference.
|
||||
/// </summary>
|
||||
Chat,
|
||||
|
||||
/// <summary>
|
||||
/// Component analysis report.
|
||||
/// </summary>
|
||||
ComponentAnalysisReport,
|
||||
|
||||
/// <summary>
|
||||
/// CPE 2.2 identifier.
|
||||
/// </summary>
|
||||
Cpe22Type,
|
||||
|
||||
/// <summary>
|
||||
/// CPE 2.3 identifier.
|
||||
/// </summary>
|
||||
Cpe23Type,
|
||||
|
||||
/// <summary>
|
||||
/// CWE reference.
|
||||
/// </summary>
|
||||
Cwe,
|
||||
|
||||
/// <summary>
|
||||
/// Documentation reference.
|
||||
/// </summary>
|
||||
Documentation,
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic analysis report.
|
||||
/// </summary>
|
||||
DynamicAnalysisReport,
|
||||
|
||||
/// <summary>
|
||||
/// End of life information.
|
||||
/// </summary>
|
||||
EolNotice,
|
||||
|
||||
/// <summary>
|
||||
/// Export control classification.
|
||||
/// </summary>
|
||||
ExportControlClassification,
|
||||
|
||||
/// <summary>
|
||||
/// Funding reference.
|
||||
/// </summary>
|
||||
Funding,
|
||||
|
||||
/// <summary>
|
||||
/// Issue tracker.
|
||||
/// </summary>
|
||||
IssueTracker,
|
||||
|
||||
/// <summary>
|
||||
/// License reference.
|
||||
/// </summary>
|
||||
License,
|
||||
|
||||
/// <summary>
|
||||
/// Mailing list reference.
|
||||
/// </summary>
|
||||
MailingList,
|
||||
|
||||
/// <summary>
|
||||
/// Maven Central reference.
|
||||
/// </summary>
|
||||
MavenCentral,
|
||||
|
||||
/// <summary>
|
||||
/// Metrics reference.
|
||||
/// </summary>
|
||||
Metrics,
|
||||
|
||||
/// <summary>
|
||||
/// NPM package reference.
|
||||
/// </summary>
|
||||
Npm,
|
||||
|
||||
/// <summary>
|
||||
/// NuGet package reference.
|
||||
/// </summary>
|
||||
Nuget,
|
||||
|
||||
/// <summary>
|
||||
/// Other reference type.
|
||||
/// </summary>
|
||||
Other,
|
||||
|
||||
/// <summary>
|
||||
/// Privacy assessment reference.
|
||||
/// </summary>
|
||||
PrivacyAssessment,
|
||||
|
||||
/// <summary>
|
||||
/// Product metadata reference.
|
||||
/// </summary>
|
||||
ProductMetadata,
|
||||
|
||||
/// <summary>
|
||||
/// Purchase order reference.
|
||||
/// </summary>
|
||||
PurchaseOrder,
|
||||
|
||||
/// <summary>
|
||||
/// Quality assessment report.
|
||||
/// </summary>
|
||||
QualityAssessmentReport,
|
||||
|
||||
/// <summary>
|
||||
/// Release history reference.
|
||||
/// </summary>
|
||||
ReleaseHistory,
|
||||
|
||||
/// <summary>
|
||||
/// Release notes reference.
|
||||
/// </summary>
|
||||
ReleaseNotes,
|
||||
|
||||
/// <summary>
|
||||
/// Risk assessment reference.
|
||||
/// </summary>
|
||||
RiskAssessment,
|
||||
|
||||
/// <summary>
|
||||
/// Runtime analysis report.
|
||||
/// </summary>
|
||||
RuntimeAnalysisReport,
|
||||
|
||||
/// <summary>
|
||||
/// Secure software development attestation.
|
||||
/// </summary>
|
||||
SecureSoftwareAttestation,
|
||||
|
||||
/// <summary>
|
||||
/// Security adversary model.
|
||||
/// </summary>
|
||||
SecurityAdversaryModel,
|
||||
|
||||
/// <summary>
|
||||
/// Security advisory.
|
||||
/// </summary>
|
||||
SecurityAdvisory,
|
||||
|
||||
/// <summary>
|
||||
/// Security fix reference.
|
||||
/// </summary>
|
||||
SecurityFix,
|
||||
|
||||
/// <summary>
|
||||
/// Security other reference.
|
||||
/// </summary>
|
||||
SecurityOther,
|
||||
|
||||
/// <summary>
|
||||
/// Security penetration test.
|
||||
/// </summary>
|
||||
SecurityPenTestReport,
|
||||
|
||||
/// <summary>
|
||||
/// Security policy.
|
||||
/// </summary>
|
||||
SecurityPolicy,
|
||||
|
||||
/// <summary>
|
||||
/// Security threat model.
|
||||
/// </summary>
|
||||
SecurityThreatModel,
|
||||
|
||||
/// <summary>
|
||||
/// Social media reference.
|
||||
/// </summary>
|
||||
SocialMedia,
|
||||
|
||||
/// <summary>
|
||||
/// Source artifact.
|
||||
/// </summary>
|
||||
SourceArtifact,
|
||||
|
||||
/// <summary>
|
||||
/// Static analysis report.
|
||||
/// </summary>
|
||||
StaticAnalysisReport,
|
||||
|
||||
/// <summary>
|
||||
/// Support reference.
|
||||
/// </summary>
|
||||
Support,
|
||||
|
||||
/// <summary>
|
||||
/// VCS (version control system) reference.
|
||||
/// </summary>
|
||||
Vcs,
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability disclosure report.
|
||||
/// </summary>
|
||||
VulnerabilityDisclosureReport,
|
||||
|
||||
/// <summary>
|
||||
/// Vulnerability exploitability assessment.
|
||||
/// </summary>
|
||||
VulnerabilityExploitabilityAssessment
|
||||
}
|
||||
225
src/__Libraries/StellaOps.Spdx3/Model/Spdx3IntegrityMethod.cs
Normal file
225
src/__Libraries/StellaOps.Spdx3/Model/Spdx3IntegrityMethod.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
// <copyright file="Spdx3IntegrityMethod.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Spdx3.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for integrity verification methods in SPDX 3.0.1.
|
||||
/// </summary>
|
||||
public abstract record Spdx3IntegrityMethod
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("@type")]
|
||||
public string? Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an optional comment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("comment")]
|
||||
public string? Comment { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a hash integrity method.
|
||||
/// </summary>
|
||||
public sealed record Spdx3Hash : Spdx3IntegrityMethod
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the hash algorithm.
|
||||
/// </summary>
|
||||
[JsonPropertyName("algorithm")]
|
||||
public required Spdx3HashAlgorithm Algorithm { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hash value (lowercase hex).
|
||||
/// </summary>
|
||||
[JsonPropertyName("hashValue")]
|
||||
public required string HashValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the normalized hash value (lowercase).
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string NormalizedHashValue => HashValue.ToLowerInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the hash value is valid hex.
|
||||
/// </summary>
|
||||
/// <returns>True if valid hex.</returns>
|
||||
public bool IsValidHex()
|
||||
{
|
||||
return HashValue.All(c => char.IsAsciiHexDigit(c));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expected hash length for the algorithm.
|
||||
/// </summary>
|
||||
/// <returns>Expected length in hex characters.</returns>
|
||||
public int GetExpectedLength() => Algorithm switch
|
||||
{
|
||||
Spdx3HashAlgorithm.Sha256 => 64,
|
||||
Spdx3HashAlgorithm.Sha384 => 96,
|
||||
Spdx3HashAlgorithm.Sha512 => 128,
|
||||
Spdx3HashAlgorithm.Sha3_256 => 64,
|
||||
Spdx3HashAlgorithm.Sha3_384 => 96,
|
||||
Spdx3HashAlgorithm.Sha3_512 => 128,
|
||||
Spdx3HashAlgorithm.Blake2b256 => 64,
|
||||
Spdx3HashAlgorithm.Blake2b384 => 96,
|
||||
Spdx3HashAlgorithm.Blake2b512 => 128,
|
||||
Spdx3HashAlgorithm.Blake3 => 64,
|
||||
Spdx3HashAlgorithm.Md5 => 32,
|
||||
Spdx3HashAlgorithm.Sha1 => 40,
|
||||
Spdx3HashAlgorithm.Adler32 => 8,
|
||||
_ => -1
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Validates the hash length matches the algorithm.
|
||||
/// </summary>
|
||||
/// <returns>True if valid length.</returns>
|
||||
public bool IsValidLength()
|
||||
{
|
||||
var expected = GetExpectedLength();
|
||||
return expected == -1 || HashValue.Length == expected;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hash algorithms supported by SPDX 3.0.1.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum Spdx3HashAlgorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// Adler-32 checksum.
|
||||
/// </summary>
|
||||
Adler32,
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE2b-256.
|
||||
/// </summary>
|
||||
Blake2b256,
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE2b-384.
|
||||
/// </summary>
|
||||
Blake2b384,
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE2b-512.
|
||||
/// </summary>
|
||||
Blake2b512,
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE3.
|
||||
/// </summary>
|
||||
Blake3,
|
||||
|
||||
/// <summary>
|
||||
/// MD2 (deprecated).
|
||||
/// </summary>
|
||||
Md2,
|
||||
|
||||
/// <summary>
|
||||
/// MD4 (deprecated).
|
||||
/// </summary>
|
||||
Md4,
|
||||
|
||||
/// <summary>
|
||||
/// MD5 (deprecated).
|
||||
/// </summary>
|
||||
Md5,
|
||||
|
||||
/// <summary>
|
||||
/// MD6.
|
||||
/// </summary>
|
||||
Md6,
|
||||
|
||||
/// <summary>
|
||||
/// SHA-1 (deprecated).
|
||||
/// </summary>
|
||||
Sha1,
|
||||
|
||||
/// <summary>
|
||||
/// SHA-224.
|
||||
/// </summary>
|
||||
Sha224,
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 (recommended).
|
||||
/// </summary>
|
||||
Sha256,
|
||||
|
||||
/// <summary>
|
||||
/// SHA-384.
|
||||
/// </summary>
|
||||
Sha384,
|
||||
|
||||
/// <summary>
|
||||
/// SHA-512 (recommended).
|
||||
/// </summary>
|
||||
Sha512,
|
||||
|
||||
/// <summary>
|
||||
/// SHA3-224.
|
||||
/// </summary>
|
||||
Sha3_224,
|
||||
|
||||
/// <summary>
|
||||
/// SHA3-256 (recommended).
|
||||
/// </summary>
|
||||
Sha3_256,
|
||||
|
||||
/// <summary>
|
||||
/// SHA3-384.
|
||||
/// </summary>
|
||||
Sha3_384,
|
||||
|
||||
/// <summary>
|
||||
/// SHA3-512 (recommended).
|
||||
/// </summary>
|
||||
Sha3_512
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for hash algorithms.
|
||||
/// </summary>
|
||||
public static class Spdx3HashAlgorithmExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether this algorithm is recommended for use.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm.</param>
|
||||
/// <returns>True if recommended.</returns>
|
||||
public static bool IsRecommended(this Spdx3HashAlgorithm algorithm) => algorithm switch
|
||||
{
|
||||
Spdx3HashAlgorithm.Sha256 => true,
|
||||
Spdx3HashAlgorithm.Sha512 => true,
|
||||
Spdx3HashAlgorithm.Sha3_256 => true,
|
||||
Spdx3HashAlgorithm.Sha3_512 => true,
|
||||
Spdx3HashAlgorithm.Blake2b256 => true,
|
||||
Spdx3HashAlgorithm.Blake2b512 => true,
|
||||
Spdx3HashAlgorithm.Blake3 => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this algorithm is deprecated.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm.</param>
|
||||
/// <returns>True if deprecated.</returns>
|
||||
public static bool IsDeprecated(this Spdx3HashAlgorithm algorithm) => algorithm switch
|
||||
{
|
||||
Spdx3HashAlgorithm.Md2 => true,
|
||||
Spdx3HashAlgorithm.Md4 => true,
|
||||
Spdx3HashAlgorithm.Md5 => true,
|
||||
Spdx3HashAlgorithm.Sha1 => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
179
src/__Libraries/StellaOps.Spdx3/Model/Spdx3ProfileIdentifier.cs
Normal file
179
src/__Libraries/StellaOps.Spdx3/Model/Spdx3ProfileIdentifier.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
// <copyright file="Spdx3ProfileIdentifier.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Spdx3.Model;
|
||||
|
||||
/// <summary>
|
||||
/// SPDX 3.0.1 profile identifiers.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum Spdx3ProfileIdentifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Core profile (required for all documents).
|
||||
/// </summary>
|
||||
Core,
|
||||
|
||||
/// <summary>
|
||||
/// Software profile (packages, files, snippets).
|
||||
/// </summary>
|
||||
Software,
|
||||
|
||||
/// <summary>
|
||||
/// Security profile (vulnerabilities, VEX).
|
||||
/// </summary>
|
||||
Security,
|
||||
|
||||
/// <summary>
|
||||
/// Licensing profile (license expressions).
|
||||
/// </summary>
|
||||
Licensing,
|
||||
|
||||
/// <summary>
|
||||
/// Build profile (build metadata, attestation).
|
||||
/// </summary>
|
||||
Build,
|
||||
|
||||
/// <summary>
|
||||
/// AI profile (ML models, datasets).
|
||||
/// </summary>
|
||||
AI,
|
||||
|
||||
/// <summary>
|
||||
/// Dataset profile (data catalogs).
|
||||
/// </summary>
|
||||
Dataset,
|
||||
|
||||
/// <summary>
|
||||
/// Lite profile (minimal SBOM).
|
||||
/// </summary>
|
||||
Lite,
|
||||
|
||||
/// <summary>
|
||||
/// Extension profile (custom extensions).
|
||||
/// </summary>
|
||||
Extension
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Profile URI constants for SPDX 3.0.1.
|
||||
/// </summary>
|
||||
public static class Spdx3ProfileUris
|
||||
{
|
||||
/// <summary>
|
||||
/// Base URI for SPDX 3.0.1 profiles.
|
||||
/// </summary>
|
||||
public const string BaseUri = "https://spdx.org/rdf/3.0.1/terms/";
|
||||
|
||||
/// <summary>
|
||||
/// Core profile URI.
|
||||
/// </summary>
|
||||
public const string Core = "https://spdx.org/rdf/3.0.1/terms/Core";
|
||||
|
||||
/// <summary>
|
||||
/// Software profile URI.
|
||||
/// </summary>
|
||||
public const string Software = "https://spdx.org/rdf/3.0.1/terms/Software";
|
||||
|
||||
/// <summary>
|
||||
/// Security profile URI.
|
||||
/// </summary>
|
||||
public const string Security = "https://spdx.org/rdf/3.0.1/terms/Security";
|
||||
|
||||
/// <summary>
|
||||
/// Licensing profile URI.
|
||||
/// </summary>
|
||||
public const string Licensing = "https://spdx.org/rdf/3.0.1/terms/Licensing";
|
||||
|
||||
/// <summary>
|
||||
/// Build profile URI.
|
||||
/// </summary>
|
||||
public const string Build = "https://spdx.org/rdf/3.0.1/terms/Build";
|
||||
|
||||
/// <summary>
|
||||
/// AI profile URI.
|
||||
/// </summary>
|
||||
public const string AI = "https://spdx.org/rdf/3.0.1/terms/AI";
|
||||
|
||||
/// <summary>
|
||||
/// Dataset profile URI.
|
||||
/// </summary>
|
||||
public const string Dataset = "https://spdx.org/rdf/3.0.1/terms/Dataset";
|
||||
|
||||
/// <summary>
|
||||
/// Lite profile URI.
|
||||
/// </summary>
|
||||
public const string Lite = "https://spdx.org/rdf/3.0.1/terms/Lite";
|
||||
|
||||
/// <summary>
|
||||
/// Extension profile URI.
|
||||
/// </summary>
|
||||
public const string Extension = "https://spdx.org/rdf/3.0.1/terms/Extension";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URI for a profile identifier.
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile identifier.</param>
|
||||
/// <returns>The profile URI.</returns>
|
||||
public static string GetUri(Spdx3ProfileIdentifier profile) => profile switch
|
||||
{
|
||||
Spdx3ProfileIdentifier.Core => Core,
|
||||
Spdx3ProfileIdentifier.Software => Software,
|
||||
Spdx3ProfileIdentifier.Security => Security,
|
||||
Spdx3ProfileIdentifier.Licensing => Licensing,
|
||||
Spdx3ProfileIdentifier.Build => Build,
|
||||
Spdx3ProfileIdentifier.AI => AI,
|
||||
Spdx3ProfileIdentifier.Dataset => Dataset,
|
||||
Spdx3ProfileIdentifier.Lite => Lite,
|
||||
Spdx3ProfileIdentifier.Extension => Extension,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(profile), profile, null)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Parses a profile URI to a profile identifier.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to parse.</param>
|
||||
/// <returns>The profile identifier, or null if not recognized.</returns>
|
||||
public static Spdx3ProfileIdentifier? ParseUri(string uri)
|
||||
{
|
||||
return uri switch
|
||||
{
|
||||
Core => Spdx3ProfileIdentifier.Core,
|
||||
Software => Spdx3ProfileIdentifier.Software,
|
||||
Security => Spdx3ProfileIdentifier.Security,
|
||||
Licensing => Spdx3ProfileIdentifier.Licensing,
|
||||
Build => Spdx3ProfileIdentifier.Build,
|
||||
AI => Spdx3ProfileIdentifier.AI,
|
||||
Dataset => Spdx3ProfileIdentifier.Dataset,
|
||||
Lite => Spdx3ProfileIdentifier.Lite,
|
||||
Extension => Spdx3ProfileIdentifier.Extension,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a profile string (name or URI) to a profile identifier.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to parse.</param>
|
||||
/// <returns>The profile identifier, or null if not recognized.</returns>
|
||||
public static Spdx3ProfileIdentifier? Parse(string value)
|
||||
{
|
||||
// Try as URI first
|
||||
var fromUri = ParseUri(value);
|
||||
if (fromUri.HasValue)
|
||||
{
|
||||
return fromUri;
|
||||
}
|
||||
|
||||
// Try as enum name
|
||||
if (Enum.TryParse<Spdx3ProfileIdentifier>(value, ignoreCase: true, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
263
src/__Libraries/StellaOps.Spdx3/Model/Spdx3Relationship.cs
Normal file
263
src/__Libraries/StellaOps.Spdx3/Model/Spdx3Relationship.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
// <copyright file="Spdx3Relationship.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Spdx3.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a relationship between SPDX 3.0.1 elements.
|
||||
/// </summary>
|
||||
public sealed record Spdx3Relationship : Spdx3Element
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the source element of the relationship.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("from")]
|
||||
public required string From { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target element(s) of the relationship.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("to")]
|
||||
public required ImmutableArray<string> To { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of relationship.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("relationshipType")]
|
||||
public required Spdx3RelationshipType RelationshipType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the completeness of the relationship.
|
||||
/// </summary>
|
||||
[JsonPropertyName("completeness")]
|
||||
public Spdx3RelationshipCompleteness? Completeness { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start time of the relationship (temporal scope).
|
||||
/// </summary>
|
||||
[JsonPropertyName("startTime")]
|
||||
public DateTimeOffset? StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end time of the relationship (temporal scope).
|
||||
/// </summary>
|
||||
[JsonPropertyName("endTime")]
|
||||
public DateTimeOffset? EndTime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SPDX 3.0.1 relationship types.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum Spdx3RelationshipType
|
||||
{
|
||||
/// <summary>
|
||||
/// Element A contains Element B.
|
||||
/// </summary>
|
||||
Contains,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is contained by Element B.
|
||||
/// </summary>
|
||||
ContainedBy,
|
||||
|
||||
/// <summary>
|
||||
/// Element A depends on Element B.
|
||||
/// </summary>
|
||||
DependsOn,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a dependency of Element B.
|
||||
/// </summary>
|
||||
DependencyOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a build tool of Element B.
|
||||
/// </summary>
|
||||
BuildToolOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a dev tool of Element B.
|
||||
/// </summary>
|
||||
DevToolOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a test tool of Element B.
|
||||
/// </summary>
|
||||
TestToolOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is documentation of Element B.
|
||||
/// </summary>
|
||||
DocumentationOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is an optional component of Element B.
|
||||
/// </summary>
|
||||
OptionalComponentOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a provided dependency of Element B.
|
||||
/// </summary>
|
||||
ProvidedDependencyOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a test of Element B.
|
||||
/// </summary>
|
||||
TestOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a test case of Element B.
|
||||
/// </summary>
|
||||
TestCaseOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a copy of Element B.
|
||||
/// </summary>
|
||||
CopyOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a file added to Element B.
|
||||
/// </summary>
|
||||
FileAddedTo,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a file deleted from Element B.
|
||||
/// </summary>
|
||||
FileDeletedFrom,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a file modified in Element B.
|
||||
/// </summary>
|
||||
FileModified,
|
||||
|
||||
/// <summary>
|
||||
/// Element A was expanded from archive Element B.
|
||||
/// </summary>
|
||||
ExpandedFromArchive,
|
||||
|
||||
/// <summary>
|
||||
/// Element A dynamically links to Element B.
|
||||
/// </summary>
|
||||
DynamicLink,
|
||||
|
||||
/// <summary>
|
||||
/// Element A statically links to Element B.
|
||||
/// </summary>
|
||||
StaticLink,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a data file of Element B.
|
||||
/// </summary>
|
||||
DataFileOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A was generated from Element B.
|
||||
/// </summary>
|
||||
GeneratedFrom,
|
||||
|
||||
/// <summary>
|
||||
/// Element A generates Element B.
|
||||
/// </summary>
|
||||
Generates,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is an ancestor of Element B.
|
||||
/// </summary>
|
||||
AncestorOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a descendant of Element B.
|
||||
/// </summary>
|
||||
DescendantOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a variant of Element B.
|
||||
/// </summary>
|
||||
VariantOf,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a distribution artifact of Element B.
|
||||
/// </summary>
|
||||
DistributionArtifact,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a patch for Element B.
|
||||
/// </summary>
|
||||
PatchFor,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a requirement for Element B.
|
||||
/// </summary>
|
||||
RequirementFor,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a specification for Element B.
|
||||
/// </summary>
|
||||
SpecificationFor,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is amended by Element B.
|
||||
/// </summary>
|
||||
AmendedBy,
|
||||
|
||||
/// <summary>
|
||||
/// Element A describes Element B.
|
||||
/// </summary>
|
||||
Describes,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is described by Element B.
|
||||
/// </summary>
|
||||
DescribedBy,
|
||||
|
||||
/// <summary>
|
||||
/// Element A has a prerequisite Element B.
|
||||
/// </summary>
|
||||
HasPrerequisite,
|
||||
|
||||
/// <summary>
|
||||
/// Element A is a prerequisite of Element B.
|
||||
/// </summary>
|
||||
PrerequisiteFor,
|
||||
|
||||
/// <summary>
|
||||
/// Element A has evidence of Element B.
|
||||
/// </summary>
|
||||
HasEvidence,
|
||||
|
||||
/// <summary>
|
||||
/// Other relationship type (requires comment).
|
||||
/// </summary>
|
||||
Other
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completeness of a relationship.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum Spdx3RelationshipCompleteness
|
||||
{
|
||||
/// <summary>
|
||||
/// The relationship is complete.
|
||||
/// </summary>
|
||||
Complete,
|
||||
|
||||
/// <summary>
|
||||
/// The relationship is incomplete.
|
||||
/// </summary>
|
||||
Incomplete,
|
||||
|
||||
/// <summary>
|
||||
/// No assertion about completeness.
|
||||
/// </summary>
|
||||
NoAssertion
|
||||
}
|
||||
587
src/__Libraries/StellaOps.Spdx3/Spdx3Parser.cs
Normal file
587
src/__Libraries/StellaOps.Spdx3/Spdx3Parser.cs
Normal file
@@ -0,0 +1,587 @@
|
||||
// <copyright file="Spdx3Parser.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Spdx3.JsonLd;
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
|
||||
namespace StellaOps.Spdx3;
|
||||
|
||||
/// <summary>
|
||||
/// SPDX 3.0.1 JSON-LD parser implementation.
|
||||
/// </summary>
|
||||
public sealed class Spdx3Parser : ISpdx3Parser
|
||||
{
|
||||
private readonly ISpdx3ContextResolver _contextResolver;
|
||||
private readonly ILogger<Spdx3Parser> _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Spdx3Parser"/> class.
|
||||
/// </summary>
|
||||
public Spdx3Parser(
|
||||
ISpdx3ContextResolver contextResolver,
|
||||
ILogger<Spdx3Parser> logger)
|
||||
{
|
||||
_contextResolver = contextResolver;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Spdx3ParseResult> ParseAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await ParseDocumentAsync(document, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse SPDX 3.0.1 JSON");
|
||||
return Spdx3ParseResult.Failed("JSON_PARSE_ERROR", ex.Message, ex.Path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Spdx3ParseResult> ParseAsync(
|
||||
string filePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return Spdx3ParseResult.Failed("FILE_NOT_FOUND", $"File not found: {filePath}");
|
||||
}
|
||||
|
||||
await using var stream = File.OpenRead(filePath);
|
||||
return await ParseAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Spdx3ParseResult> ParseFromJsonAsync(
|
||||
string json,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(json));
|
||||
return await ParseAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<Spdx3ParseResult> ParseDocumentAsync(
|
||||
JsonDocument document,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var root = document.RootElement;
|
||||
var errors = new List<Spdx3ParseError>();
|
||||
var warnings = new List<Spdx3ParseWarning>();
|
||||
|
||||
// Check for @context (JSON-LD indicator)
|
||||
if (!root.TryGetProperty("@context", out var contextElement))
|
||||
{
|
||||
return Spdx3ParseResult.Failed("MISSING_CONTEXT", "No @context found. Is this an SPDX 3.0.1 JSON-LD document?");
|
||||
}
|
||||
|
||||
// Resolve context (for future expansion support)
|
||||
var contextRef = GetContextReference(contextElement);
|
||||
if (!string.IsNullOrEmpty(contextRef))
|
||||
{
|
||||
await _contextResolver.ResolveAsync(contextRef, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Parse @graph (main element array)
|
||||
var elements = new List<Spdx3Element>();
|
||||
var creationInfos = new List<Spdx3CreationInfo>();
|
||||
var profiles = new HashSet<Spdx3ProfileIdentifier>();
|
||||
Spdx3SpdxDocument? spdxDocument = null;
|
||||
|
||||
if (root.TryGetProperty("@graph", out var graphElement) && graphElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var element in graphElement.EnumerateArray())
|
||||
{
|
||||
var parsed = ParseElement(element, errors, warnings);
|
||||
if (parsed != null)
|
||||
{
|
||||
elements.Add(parsed);
|
||||
|
||||
if (parsed is Spdx3SpdxDocument doc)
|
||||
{
|
||||
spdxDocument = doc;
|
||||
}
|
||||
}
|
||||
|
||||
// Also extract CreationInfo
|
||||
var creationInfo = ExtractCreationInfo(element, errors);
|
||||
if (creationInfo != null)
|
||||
{
|
||||
creationInfos.Add(creationInfo);
|
||||
foreach (var profile in creationInfo.Profile)
|
||||
{
|
||||
profiles.Add(profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single document format (no @graph)
|
||||
var parsed = ParseElement(root, errors, warnings);
|
||||
if (parsed != null)
|
||||
{
|
||||
elements.Add(parsed);
|
||||
|
||||
if (parsed is Spdx3SpdxDocument doc)
|
||||
{
|
||||
spdxDocument = doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
return Spdx3ParseResult.Failed(errors, warnings);
|
||||
}
|
||||
|
||||
var result = new Spdx3Document(elements, creationInfos, profiles, spdxDocument);
|
||||
return Spdx3ParseResult.Succeeded(result, warnings);
|
||||
}
|
||||
|
||||
private Spdx3Element? ParseElement(
|
||||
JsonElement element,
|
||||
List<Spdx3ParseError> errors,
|
||||
List<Spdx3ParseWarning> warnings)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get @type to determine element type
|
||||
var type = GetStringProperty(element, "@type") ?? GetStringProperty(element, "type");
|
||||
if (string.IsNullOrEmpty(type))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get spdxId (required)
|
||||
var spdxId = GetStringProperty(element, "spdxId") ?? GetStringProperty(element, "@id");
|
||||
if (string.IsNullOrEmpty(spdxId))
|
||||
{
|
||||
warnings.Add(new Spdx3ParseWarning("MISSING_SPDX_ID", $"Element of type {type} has no spdxId"));
|
||||
return null;
|
||||
}
|
||||
|
||||
return type switch
|
||||
{
|
||||
"software_Package" or "Package" or "spdx:software_Package" =>
|
||||
ParsePackage(element, spdxId),
|
||||
"software_File" or "File" or "spdx:software_File" =>
|
||||
ParseFile(element, spdxId),
|
||||
"software_Snippet" or "Snippet" or "spdx:software_Snippet" =>
|
||||
ParseSnippet(element, spdxId),
|
||||
"software_SpdxDocument" or "SpdxDocument" or "spdx:software_SpdxDocument" =>
|
||||
ParseSpdxDocument(element, spdxId),
|
||||
"Relationship" or "spdx:Relationship" =>
|
||||
ParseRelationship(element, spdxId),
|
||||
"Person" or "spdx:Person" =>
|
||||
ParseAgent<Spdx3Person>(element, spdxId),
|
||||
"Organization" or "spdx:Organization" =>
|
||||
ParseAgent<Spdx3Organization>(element, spdxId),
|
||||
"Tool" or "spdx:Tool" =>
|
||||
ParseAgent<Spdx3Tool>(element, spdxId),
|
||||
_ => ParseGenericElement(element, spdxId, type, warnings)
|
||||
};
|
||||
}
|
||||
|
||||
private Spdx3Package ParsePackage(JsonElement element, string spdxId)
|
||||
{
|
||||
return new Spdx3Package
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
Type = GetStringProperty(element, "@type"),
|
||||
Name = GetStringProperty(element, "name"),
|
||||
Summary = GetStringProperty(element, "summary"),
|
||||
Description = GetStringProperty(element, "description"),
|
||||
Comment = GetStringProperty(element, "comment"),
|
||||
PackageVersion = GetStringProperty(element, "packageVersion"),
|
||||
DownloadLocation = GetStringProperty(element, "downloadLocation"),
|
||||
PackageUrl = GetStringProperty(element, "packageUrl"),
|
||||
HomePage = GetStringProperty(element, "homePage"),
|
||||
SourceInfo = GetStringProperty(element, "sourceInfo"),
|
||||
CopyrightText = GetStringProperty(element, "copyrightText"),
|
||||
SuppliedBy = GetStringProperty(element, "suppliedBy"),
|
||||
VerifiedUsing = ParseIntegrityMethods(element),
|
||||
ExternalRef = ParseExternalRefs(element),
|
||||
ExternalIdentifier = ParseExternalIdentifiers(element),
|
||||
OriginatedBy = GetStringArrayProperty(element, "originatedBy"),
|
||||
AttributionText = GetStringArrayProperty(element, "attributionText")
|
||||
};
|
||||
}
|
||||
|
||||
private Spdx3File ParseFile(JsonElement element, string spdxId)
|
||||
{
|
||||
return new Spdx3File
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
Type = GetStringProperty(element, "@type"),
|
||||
Name = GetStringProperty(element, "name") ?? string.Empty,
|
||||
Summary = GetStringProperty(element, "summary"),
|
||||
Description = GetStringProperty(element, "description"),
|
||||
Comment = GetStringProperty(element, "comment"),
|
||||
ContentType = GetStringProperty(element, "contentType"),
|
||||
CopyrightText = GetStringProperty(element, "copyrightText"),
|
||||
VerifiedUsing = ParseIntegrityMethods(element),
|
||||
ExternalRef = ParseExternalRefs(element),
|
||||
ExternalIdentifier = ParseExternalIdentifiers(element)
|
||||
};
|
||||
}
|
||||
|
||||
private Spdx3Snippet ParseSnippet(JsonElement element, string spdxId)
|
||||
{
|
||||
return new Spdx3Snippet
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
Type = GetStringProperty(element, "@type"),
|
||||
Name = GetStringProperty(element, "name"),
|
||||
Summary = GetStringProperty(element, "summary"),
|
||||
Description = GetStringProperty(element, "description"),
|
||||
Comment = GetStringProperty(element, "comment"),
|
||||
SnippetFromFile = GetStringProperty(element, "snippetFromFile") ?? string.Empty,
|
||||
CopyrightText = GetStringProperty(element, "copyrightText"),
|
||||
VerifiedUsing = ParseIntegrityMethods(element),
|
||||
ExternalRef = ParseExternalRefs(element),
|
||||
ExternalIdentifier = ParseExternalIdentifiers(element)
|
||||
};
|
||||
}
|
||||
|
||||
private Spdx3SpdxDocument ParseSpdxDocument(JsonElement element, string spdxId)
|
||||
{
|
||||
return new Spdx3SpdxDocument
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
Type = GetStringProperty(element, "@type"),
|
||||
Name = GetStringProperty(element, "name"),
|
||||
Summary = GetStringProperty(element, "summary"),
|
||||
Description = GetStringProperty(element, "description"),
|
||||
Comment = GetStringProperty(element, "comment"),
|
||||
Element = GetStringArrayProperty(element, "element"),
|
||||
RootElement = GetStringArrayProperty(element, "rootElement"),
|
||||
VerifiedUsing = ParseIntegrityMethods(element),
|
||||
ExternalRef = ParseExternalRefs(element),
|
||||
ExternalIdentifier = ParseExternalIdentifiers(element)
|
||||
};
|
||||
}
|
||||
|
||||
private Spdx3Relationship ParseRelationship(JsonElement element, string spdxId)
|
||||
{
|
||||
var toValue = GetStringProperty(element, "to");
|
||||
var toArray = toValue != null
|
||||
? [toValue]
|
||||
: GetStringArrayProperty(element, "to");
|
||||
|
||||
var relationshipType = GetStringProperty(element, "relationshipType") ?? "Other";
|
||||
if (!Enum.TryParse<Spdx3RelationshipType>(relationshipType, ignoreCase: true, out var relType))
|
||||
{
|
||||
relType = Spdx3RelationshipType.Other;
|
||||
}
|
||||
|
||||
return new Spdx3Relationship
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
Type = GetStringProperty(element, "@type"),
|
||||
From = GetStringProperty(element, "from") ?? string.Empty,
|
||||
To = toArray,
|
||||
RelationshipType = relType,
|
||||
Comment = GetStringProperty(element, "comment"),
|
||||
VerifiedUsing = ParseIntegrityMethods(element),
|
||||
ExternalRef = ParseExternalRefs(element),
|
||||
ExternalIdentifier = ParseExternalIdentifiers(element)
|
||||
};
|
||||
}
|
||||
|
||||
private T ParseAgent<T>(JsonElement element, string spdxId) where T : Spdx3Element
|
||||
{
|
||||
var name = GetStringProperty(element, "name") ?? string.Empty;
|
||||
|
||||
if (typeof(T) == typeof(Spdx3Person))
|
||||
{
|
||||
return (T)(object)new Spdx3Person
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
Type = GetStringProperty(element, "@type"),
|
||||
Name = name
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(Spdx3Organization))
|
||||
{
|
||||
return (T)(object)new Spdx3Organization
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
Type = GetStringProperty(element, "@type"),
|
||||
Name = name
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(Spdx3Tool))
|
||||
{
|
||||
return (T)(object)new Spdx3Tool
|
||||
{
|
||||
SpdxId = spdxId,
|
||||
Type = GetStringProperty(element, "@type"),
|
||||
Name = name
|
||||
};
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unsupported agent type: {typeof(T)}");
|
||||
}
|
||||
|
||||
private Spdx3Element? ParseGenericElement(
|
||||
JsonElement element,
|
||||
string spdxId,
|
||||
string type,
|
||||
List<Spdx3ParseWarning> warnings)
|
||||
{
|
||||
// For unknown types, we could create a generic element
|
||||
// For now, log a warning and skip
|
||||
warnings.Add(new Spdx3ParseWarning("UNKNOWN_TYPE", $"Unknown element type: {type}", spdxId));
|
||||
return null;
|
||||
}
|
||||
|
||||
private Spdx3CreationInfo? ExtractCreationInfo(
|
||||
JsonElement element,
|
||||
List<Spdx3ParseError> errors)
|
||||
{
|
||||
if (!element.TryGetProperty("creationInfo", out var ciElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ciElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
// Reference to CreationInfo - will be resolved later
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ciElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var specVersion = GetStringProperty(ciElement, "specVersion");
|
||||
if (string.IsNullOrEmpty(specVersion))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var createdStr = GetStringProperty(ciElement, "created");
|
||||
if (!DateTimeOffset.TryParse(createdStr, out var created))
|
||||
{
|
||||
created = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
var profileStrings = GetStringArrayProperty(ciElement, "profile");
|
||||
var profiles = profileStrings
|
||||
.Select(p => Spdx3ProfileUris.Parse(p))
|
||||
.Where(p => p.HasValue)
|
||||
.Select(p => p!.Value)
|
||||
.ToImmutableArray();
|
||||
|
||||
return new Spdx3CreationInfo
|
||||
{
|
||||
Id = GetStringProperty(ciElement, "@id"),
|
||||
Type = GetStringProperty(ciElement, "@type"),
|
||||
SpecVersion = specVersion,
|
||||
Created = created,
|
||||
CreatedBy = GetStringArrayProperty(ciElement, "createdBy"),
|
||||
CreatedUsing = GetStringArrayProperty(ciElement, "createdUsing"),
|
||||
Profile = profiles,
|
||||
DataLicense = GetStringProperty(ciElement, "dataLicense"),
|
||||
Comment = GetStringProperty(ciElement, "comment")
|
||||
};
|
||||
}
|
||||
|
||||
private ImmutableArray<Spdx3IntegrityMethod> ParseIntegrityMethods(JsonElement element)
|
||||
{
|
||||
if (!element.TryGetProperty("verifiedUsing", out var verifiedUsing))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (verifiedUsing.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var methods = new List<Spdx3IntegrityMethod>();
|
||||
foreach (var item in verifiedUsing.EnumerateArray())
|
||||
{
|
||||
var algorithm = GetStringProperty(item, "algorithm");
|
||||
var hashValue = GetStringProperty(item, "hashValue");
|
||||
|
||||
if (!string.IsNullOrEmpty(algorithm) && !string.IsNullOrEmpty(hashValue))
|
||||
{
|
||||
if (Enum.TryParse<Spdx3HashAlgorithm>(algorithm.Replace("-", "_"), ignoreCase: true, out var algo))
|
||||
{
|
||||
methods.Add(new Spdx3Hash
|
||||
{
|
||||
Type = GetStringProperty(item, "@type"),
|
||||
Algorithm = algo,
|
||||
HashValue = hashValue.ToLowerInvariant(),
|
||||
Comment = GetStringProperty(item, "comment")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [.. methods];
|
||||
}
|
||||
|
||||
private ImmutableArray<Spdx3ExternalRef> ParseExternalRefs(JsonElement element)
|
||||
{
|
||||
if (!element.TryGetProperty("externalRef", out var externalRef))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (externalRef.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var refs = new List<Spdx3ExternalRef>();
|
||||
foreach (var item in externalRef.EnumerateArray())
|
||||
{
|
||||
refs.Add(new Spdx3ExternalRef
|
||||
{
|
||||
Type = GetStringProperty(item, "@type"),
|
||||
Locator = GetStringArrayProperty(item, "locator"),
|
||||
ContentType = GetStringProperty(item, "contentType"),
|
||||
Comment = GetStringProperty(item, "comment")
|
||||
});
|
||||
}
|
||||
|
||||
return [.. refs];
|
||||
}
|
||||
|
||||
private ImmutableArray<Spdx3ExternalIdentifier> ParseExternalIdentifiers(JsonElement element)
|
||||
{
|
||||
if (!element.TryGetProperty("externalIdentifier", out var externalId))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (externalId.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var identifiers = new List<Spdx3ExternalIdentifier>();
|
||||
foreach (var item in externalId.EnumerateArray())
|
||||
{
|
||||
var identifier = GetStringProperty(item, "identifier");
|
||||
if (string.IsNullOrEmpty(identifier))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var typeStr = GetStringProperty(item, "externalIdentifierType");
|
||||
Spdx3ExternalIdentifierType? idType = null;
|
||||
if (!string.IsNullOrEmpty(typeStr) &&
|
||||
Enum.TryParse<Spdx3ExternalIdentifierType>(typeStr, ignoreCase: true, out var parsed))
|
||||
{
|
||||
idType = parsed;
|
||||
}
|
||||
|
||||
identifiers.Add(new Spdx3ExternalIdentifier
|
||||
{
|
||||
Type = GetStringProperty(item, "@type"),
|
||||
ExternalIdentifierType = idType,
|
||||
Identifier = identifier,
|
||||
Comment = GetStringProperty(item, "comment"),
|
||||
IssuingAuthority = GetStringProperty(item, "issuingAuthority")
|
||||
});
|
||||
}
|
||||
|
||||
return [.. identifiers];
|
||||
}
|
||||
|
||||
private static string? GetStringProperty(JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.TryGetProperty(propertyName, out var prop) &&
|
||||
prop.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return prop.GetString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> GetStringArrayProperty(JsonElement element, string propertyName)
|
||||
{
|
||||
if (!element.TryGetProperty(propertyName, out var prop))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (prop.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var value = prop.GetString();
|
||||
return value != null ? [value] : [];
|
||||
}
|
||||
|
||||
if (prop.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var list = new List<string>();
|
||||
foreach (var item in prop.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var value = item.GetString();
|
||||
if (value != null)
|
||||
{
|
||||
list.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [.. list];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static string? GetContextReference(JsonElement contextElement)
|
||||
{
|
||||
if (contextElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return contextElement.GetString();
|
||||
}
|
||||
|
||||
if (contextElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in contextElement.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return item.GetString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
203
src/__Libraries/StellaOps.Spdx3/Spdx3VersionDetector.cs
Normal file
203
src/__Libraries/StellaOps.Spdx3/Spdx3VersionDetector.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
// <copyright file="Spdx3VersionDetector.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Spdx3;
|
||||
|
||||
/// <summary>
|
||||
/// Detects the SPDX version of a document.
|
||||
/// </summary>
|
||||
public static class Spdx3VersionDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Detected SPDX version.
|
||||
/// </summary>
|
||||
public enum SpdxVersion
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown version.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// SPDX 2.2.
|
||||
/// </summary>
|
||||
Spdx22,
|
||||
|
||||
/// <summary>
|
||||
/// SPDX 2.3.
|
||||
/// </summary>
|
||||
Spdx23,
|
||||
|
||||
/// <summary>
|
||||
/// SPDX 3.0.1.
|
||||
/// </summary>
|
||||
Spdx301
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Version detection result.
|
||||
/// </summary>
|
||||
/// <param name="Version">The detected version.</param>
|
||||
/// <param name="VersionString">The raw version string if found.</param>
|
||||
/// <param name="IsJsonLd">Whether the document uses JSON-LD format.</param>
|
||||
public readonly record struct DetectionResult(
|
||||
SpdxVersion Version,
|
||||
string? VersionString,
|
||||
bool IsJsonLd);
|
||||
|
||||
/// <summary>
|
||||
/// Detects the SPDX version from a JSON document.
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON content.</param>
|
||||
/// <returns>The detection result.</returns>
|
||||
public static DetectionResult Detect(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return Detect(document.RootElement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects the SPDX version from a JSON element.
|
||||
/// </summary>
|
||||
/// <param name="root">The root JSON element.</param>
|
||||
/// <returns>The detection result.</returns>
|
||||
public static DetectionResult Detect(JsonElement root)
|
||||
{
|
||||
// Check for JSON-LD @context (SPDX 3.x indicator)
|
||||
if (root.TryGetProperty("@context", out var context))
|
||||
{
|
||||
var contextStr = GetContextString(context);
|
||||
if (!string.IsNullOrEmpty(contextStr))
|
||||
{
|
||||
// Check for specific 3.0.1 context
|
||||
if (contextStr.Contains("3.0.1", StringComparison.OrdinalIgnoreCase) ||
|
||||
contextStr.Contains("spdx.org/rdf/3", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new DetectionResult(SpdxVersion.Spdx301, "3.0.1", true);
|
||||
}
|
||||
|
||||
// Generic 3.x detection
|
||||
if (contextStr.Contains("spdx.org/rdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new DetectionResult(SpdxVersion.Spdx301, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Has @context but couldn't determine specific version
|
||||
return new DetectionResult(SpdxVersion.Spdx301, null, true);
|
||||
}
|
||||
|
||||
// Check for SPDX 2.x spdxVersion field
|
||||
if (root.TryGetProperty("spdxVersion", out var spdxVersion) &&
|
||||
spdxVersion.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var versionStr = spdxVersion.GetString();
|
||||
if (!string.IsNullOrEmpty(versionStr))
|
||||
{
|
||||
if (versionStr.Contains("2.3", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new DetectionResult(SpdxVersion.Spdx23, versionStr, false);
|
||||
}
|
||||
|
||||
if (versionStr.Contains("2.2", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new DetectionResult(SpdxVersion.Spdx22, versionStr, false);
|
||||
}
|
||||
|
||||
// Older 2.x versions
|
||||
if (versionStr.StartsWith("SPDX-2", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new DetectionResult(SpdxVersion.Spdx22, versionStr, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for creationInfo.specVersion (SPDX 3.x in @graph format)
|
||||
if (root.TryGetProperty("@graph", out var graph) && graph.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var element in graph.EnumerateArray())
|
||||
{
|
||||
if (element.TryGetProperty("creationInfo", out var creationInfo) &&
|
||||
creationInfo.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (creationInfo.TryGetProperty("specVersion", out var specVersion) &&
|
||||
specVersion.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var specVersionStr = specVersion.GetString();
|
||||
if (specVersionStr == "3.0.1")
|
||||
{
|
||||
return new DetectionResult(SpdxVersion.Spdx301, specVersionStr, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DetectionResult(SpdxVersion.Unknown, null, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects the SPDX version from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The input stream.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The detection result.</returns>
|
||||
public static async Task<DetectionResult> DetectAsync(
|
||||
Stream stream,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return Detect(document.RootElement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recommended parser for the detected version.
|
||||
/// </summary>
|
||||
/// <param name="version">The detected version.</param>
|
||||
/// <returns>Parser recommendation.</returns>
|
||||
public static string GetParserRecommendation(SpdxVersion version) => version switch
|
||||
{
|
||||
SpdxVersion.Spdx22 => "Use SpdxParser (SPDX 2.x parser)",
|
||||
SpdxVersion.Spdx23 => "Use SpdxParser (SPDX 2.x parser)",
|
||||
SpdxVersion.Spdx301 => "Use Spdx3Parser (SPDX 3.0.1 parser)",
|
||||
_ => "Unknown format - manual inspection required"
|
||||
};
|
||||
|
||||
private static string? GetContextString(JsonElement context)
|
||||
{
|
||||
if (context.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return context.GetString();
|
||||
}
|
||||
|
||||
if (context.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in context.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var str = item.GetString();
|
||||
if (!string.IsNullOrEmpty(str) && str.Contains("spdx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return first string if no spdx-specific one found
|
||||
foreach (var item in context.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return item.GetString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
25
src/__Libraries/StellaOps.Spdx3/StellaOps.Spdx3.csproj
Normal file
25
src/__Libraries/StellaOps.Spdx3/StellaOps.Spdx3.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<RootNamespace>StellaOps.Spdx3</RootNamespace>
|
||||
<AssemblyName>StellaOps.Spdx3</AssemblyName>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Description>SPDX 3.0.1 parsing library with full profile support for StellaOps</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
155
src/__Libraries/StellaOps.Spdx3/Validation/ISpdx3Validator.cs
Normal file
155
src/__Libraries/StellaOps.Spdx3/Validation/ISpdx3Validator.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
// <copyright file="ISpdx3Validator.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Spdx3.Model;
|
||||
|
||||
namespace StellaOps.Spdx3.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for SPDX 3.0.1 document validation.
|
||||
/// </summary>
|
||||
public interface ISpdx3Validator
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates an SPDX 3.0.1 document.
|
||||
/// </summary>
|
||||
/// <param name="document">The document to validate.</param>
|
||||
/// <param name="options">Validation options.</param>
|
||||
/// <returns>The validation result.</returns>
|
||||
Spdx3ValidationResult Validate(
|
||||
Spdx3Document document,
|
||||
Spdx3ValidationOptions? options = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for SPDX 3.0.1 validation.
|
||||
/// </summary>
|
||||
public sealed class Spdx3ValidationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets whether to validate profile-specific requirements.
|
||||
/// </summary>
|
||||
public bool ValidateProfiles { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to validate relationships.
|
||||
/// </summary>
|
||||
public bool ValidateRelationships { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to validate external identifiers (PURL format, etc.).
|
||||
/// </summary>
|
||||
public bool ValidateExternalIdentifiers { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to validate hash values.
|
||||
/// </summary>
|
||||
public bool ValidateHashes { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether warnings should be treated as errors.
|
||||
/// </summary>
|
||||
public bool TreatWarningsAsErrors { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum required profiles.
|
||||
/// </summary>
|
||||
public ImmutableHashSet<Spdx3ProfileIdentifier> RequiredProfiles { get; set; } =
|
||||
ImmutableHashSet<Spdx3ProfileIdentifier>.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of SPDX 3.0.1 validation.
|
||||
/// </summary>
|
||||
public sealed record Spdx3ValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether validation passed.
|
||||
/// </summary>
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets validation errors.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Spdx3ValidationIssue> Errors { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets validation warnings.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Spdx3ValidationIssue> Warnings { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets validation information.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Spdx3ValidationIssue> Info { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Creates a valid result.
|
||||
/// </summary>
|
||||
public static Spdx3ValidationResult Valid(
|
||||
IReadOnlyList<Spdx3ValidationIssue>? warnings = null,
|
||||
IReadOnlyList<Spdx3ValidationIssue>? info = null)
|
||||
{
|
||||
return new Spdx3ValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
Warnings = warnings ?? [],
|
||||
Info = info ?? []
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an invalid result.
|
||||
/// </summary>
|
||||
public static Spdx3ValidationResult Invalid(
|
||||
IReadOnlyList<Spdx3ValidationIssue> errors,
|
||||
IReadOnlyList<Spdx3ValidationIssue>? warnings = null,
|
||||
IReadOnlyList<Spdx3ValidationIssue>? info = null)
|
||||
{
|
||||
return new Spdx3ValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Errors = errors,
|
||||
Warnings = warnings ?? [],
|
||||
Info = info ?? []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a validation issue.
|
||||
/// </summary>
|
||||
/// <param name="Code">Issue code.</param>
|
||||
/// <param name="Message">Issue message.</param>
|
||||
/// <param name="Severity">Issue severity.</param>
|
||||
/// <param name="ElementId">The SPDX ID of the affected element.</param>
|
||||
/// <param name="Path">JSON path or property name.</param>
|
||||
public sealed record Spdx3ValidationIssue(
|
||||
string Code,
|
||||
string Message,
|
||||
Spdx3ValidationSeverity Severity,
|
||||
string? ElementId = null,
|
||||
string? Path = null);
|
||||
|
||||
/// <summary>
|
||||
/// Validation issue severity.
|
||||
/// </summary>
|
||||
public enum Spdx3ValidationSeverity
|
||||
{
|
||||
/// <summary>
|
||||
/// Informational message.
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// Warning (non-fatal).
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Error (fatal).
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
344
src/__Libraries/StellaOps.Spdx3/Validation/Spdx3Validator.cs
Normal file
344
src/__Libraries/StellaOps.Spdx3/Validation/Spdx3Validator.cs
Normal file
@@ -0,0 +1,344 @@
|
||||
// <copyright file="Spdx3Validator.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using StellaOps.Spdx3.Model;
|
||||
using StellaOps.Spdx3.Model.Software;
|
||||
|
||||
namespace StellaOps.Spdx3.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// SPDX 3.0.1 document validator implementation.
|
||||
/// </summary>
|
||||
public sealed partial class Spdx3Validator : ISpdx3Validator
|
||||
{
|
||||
[GeneratedRegex(@"^pkg:[a-z]+/.+", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex PurlPattern();
|
||||
|
||||
[GeneratedRegex(@"^cpe:2\.3:[aho\*\-]", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex Cpe23Pattern();
|
||||
|
||||
[GeneratedRegex(@"^[a-fA-F0-9]+$")]
|
||||
private static partial Regex HexPattern();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Spdx3ValidationResult Validate(
|
||||
Spdx3Document document,
|
||||
Spdx3ValidationOptions? options = null)
|
||||
{
|
||||
options ??= new Spdx3ValidationOptions();
|
||||
|
||||
var errors = new List<Spdx3ValidationIssue>();
|
||||
var warnings = new List<Spdx3ValidationIssue>();
|
||||
var info = new List<Spdx3ValidationIssue>();
|
||||
|
||||
// Core validation
|
||||
ValidateCore(document, errors, warnings, info);
|
||||
|
||||
// Profile validation
|
||||
if (options.ValidateProfiles)
|
||||
{
|
||||
ValidateProfiles(document, options, errors, warnings, info);
|
||||
}
|
||||
|
||||
// Relationship validation
|
||||
if (options.ValidateRelationships)
|
||||
{
|
||||
ValidateRelationships(document, errors, warnings, info);
|
||||
}
|
||||
|
||||
// External identifier validation
|
||||
if (options.ValidateExternalIdentifiers)
|
||||
{
|
||||
ValidateExternalIdentifiers(document, errors, warnings, info);
|
||||
}
|
||||
|
||||
// Hash validation
|
||||
if (options.ValidateHashes)
|
||||
{
|
||||
ValidateHashes(document, errors, warnings, info);
|
||||
}
|
||||
|
||||
// Treat warnings as errors if configured
|
||||
if (options.TreatWarningsAsErrors && warnings.Count > 0)
|
||||
{
|
||||
errors.AddRange(warnings.Select(w => w with { Severity = Spdx3ValidationSeverity.Error }));
|
||||
warnings.Clear();
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
return Spdx3ValidationResult.Invalid(errors, warnings, info);
|
||||
}
|
||||
|
||||
return Spdx3ValidationResult.Valid(warnings, info);
|
||||
}
|
||||
|
||||
private static void ValidateCore(
|
||||
Spdx3Document document,
|
||||
List<Spdx3ValidationIssue> errors,
|
||||
List<Spdx3ValidationIssue> warnings,
|
||||
List<Spdx3ValidationIssue> info)
|
||||
{
|
||||
// Must have at least one element
|
||||
if (document.AllElements.Length == 0)
|
||||
{
|
||||
errors.Add(new Spdx3ValidationIssue(
|
||||
"EMPTY_DOCUMENT",
|
||||
"Document contains no elements",
|
||||
Spdx3ValidationSeverity.Error));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate each element has required fields
|
||||
foreach (var element in document.AllElements)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(element.SpdxId))
|
||||
{
|
||||
errors.Add(new Spdx3ValidationIssue(
|
||||
"MISSING_SPDX_ID",
|
||||
"Element is missing spdxId",
|
||||
Spdx3ValidationSeverity.Error,
|
||||
Path: element.Type));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate spdxIds
|
||||
var duplicates = document.AllElements
|
||||
.GroupBy(e => e.SpdxId)
|
||||
.Where(g => g.Count() > 1)
|
||||
.Select(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var dup in duplicates)
|
||||
{
|
||||
errors.Add(new Spdx3ValidationIssue(
|
||||
"DUPLICATE_SPDX_ID",
|
||||
$"Duplicate spdxId found: {dup}",
|
||||
Spdx3ValidationSeverity.Error,
|
||||
ElementId: dup));
|
||||
}
|
||||
|
||||
// Validate SpdxDocument if present
|
||||
if (document.SpdxDocument != null)
|
||||
{
|
||||
if (document.SpdxDocument.RootElement.IsEmpty)
|
||||
{
|
||||
warnings.Add(new Spdx3ValidationIssue(
|
||||
"NO_ROOT_ELEMENT",
|
||||
"SpdxDocument has no rootElement specified",
|
||||
Spdx3ValidationSeverity.Warning,
|
||||
ElementId: document.SpdxDocument.SpdxId));
|
||||
}
|
||||
}
|
||||
|
||||
// Info about document contents
|
||||
info.Add(new Spdx3ValidationIssue(
|
||||
"DOCUMENT_STATS",
|
||||
$"Document contains {document.Packages.Length} packages, " +
|
||||
$"{document.Files.Length} files, " +
|
||||
$"{document.Relationships.Length} relationships",
|
||||
Spdx3ValidationSeverity.Info));
|
||||
}
|
||||
|
||||
private static void ValidateProfiles(
|
||||
Spdx3Document document,
|
||||
Spdx3ValidationOptions options,
|
||||
List<Spdx3ValidationIssue> errors,
|
||||
List<Spdx3ValidationIssue> warnings,
|
||||
List<Spdx3ValidationIssue> info)
|
||||
{
|
||||
// Check required profiles
|
||||
foreach (var required in options.RequiredProfiles)
|
||||
{
|
||||
if (!document.ConformsTo(required))
|
||||
{
|
||||
errors.Add(new Spdx3ValidationIssue(
|
||||
"MISSING_REQUIRED_PROFILE",
|
||||
$"Document does not conform to required profile: {required}",
|
||||
Spdx3ValidationSeverity.Error));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Software profile requirements
|
||||
if (document.ConformsTo(Spdx3ProfileIdentifier.Software))
|
||||
{
|
||||
foreach (var package in document.Packages)
|
||||
{
|
||||
// Software profile packages should have a name
|
||||
if (string.IsNullOrWhiteSpace(package.Name))
|
||||
{
|
||||
warnings.Add(new Spdx3ValidationIssue(
|
||||
"PACKAGE_MISSING_NAME",
|
||||
"Package is missing name (recommended for Software profile)",
|
||||
Spdx3ValidationSeverity.Warning,
|
||||
ElementId: package.SpdxId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Lite profile requirements
|
||||
if (document.ConformsTo(Spdx3ProfileIdentifier.Lite))
|
||||
{
|
||||
foreach (var package in document.Packages)
|
||||
{
|
||||
// Lite profile requires name and version
|
||||
if (string.IsNullOrWhiteSpace(package.Name))
|
||||
{
|
||||
errors.Add(new Spdx3ValidationIssue(
|
||||
"LITE_PROFILE_MISSING_NAME",
|
||||
"Lite profile requires package name",
|
||||
Spdx3ValidationSeverity.Error,
|
||||
ElementId: package.SpdxId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Report detected profiles
|
||||
var profileNames = string.Join(", ", document.Profiles.Select(p => p.ToString()));
|
||||
if (!string.IsNullOrEmpty(profileNames))
|
||||
{
|
||||
info.Add(new Spdx3ValidationIssue(
|
||||
"DETECTED_PROFILES",
|
||||
$"Document conforms to profiles: {profileNames}",
|
||||
Spdx3ValidationSeverity.Info));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateRelationships(
|
||||
Spdx3Document document,
|
||||
List<Spdx3ValidationIssue> errors,
|
||||
List<Spdx3ValidationIssue> warnings,
|
||||
List<Spdx3ValidationIssue> info)
|
||||
{
|
||||
var elementIds = document.Elements.Select(e => e.SpdxId).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (var relationship in document.Relationships)
|
||||
{
|
||||
// Validate 'from' reference
|
||||
if (!elementIds.Contains(relationship.From))
|
||||
{
|
||||
warnings.Add(new Spdx3ValidationIssue(
|
||||
"DANGLING_RELATIONSHIP_FROM",
|
||||
$"Relationship 'from' references unknown element: {relationship.From}",
|
||||
Spdx3ValidationSeverity.Warning,
|
||||
ElementId: relationship.SpdxId));
|
||||
}
|
||||
|
||||
// Validate 'to' references
|
||||
foreach (var to in relationship.To)
|
||||
{
|
||||
if (!elementIds.Contains(to))
|
||||
{
|
||||
warnings.Add(new Spdx3ValidationIssue(
|
||||
"DANGLING_RELATIONSHIP_TO",
|
||||
$"Relationship 'to' references unknown element: {to}",
|
||||
Spdx3ValidationSeverity.Warning,
|
||||
ElementId: relationship.SpdxId));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate relationship has targets
|
||||
if (relationship.To.IsEmpty)
|
||||
{
|
||||
errors.Add(new Spdx3ValidationIssue(
|
||||
"EMPTY_RELATIONSHIP_TO",
|
||||
"Relationship has no 'to' targets",
|
||||
Spdx3ValidationSeverity.Error,
|
||||
ElementId: relationship.SpdxId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateExternalIdentifiers(
|
||||
Spdx3Document document,
|
||||
List<Spdx3ValidationIssue> errors,
|
||||
List<Spdx3ValidationIssue> warnings,
|
||||
List<Spdx3ValidationIssue> info)
|
||||
{
|
||||
foreach (var element in document.Elements)
|
||||
{
|
||||
foreach (var extId in element.ExternalIdentifier)
|
||||
{
|
||||
switch (extId.ExternalIdentifierType)
|
||||
{
|
||||
case Spdx3ExternalIdentifierType.PackageUrl:
|
||||
if (!PurlPattern().IsMatch(extId.Identifier))
|
||||
{
|
||||
warnings.Add(new Spdx3ValidationIssue(
|
||||
"INVALID_PURL_FORMAT",
|
||||
$"Invalid PURL format: {extId.Identifier}",
|
||||
Spdx3ValidationSeverity.Warning,
|
||||
ElementId: element.SpdxId,
|
||||
Path: "externalIdentifier/identifier"));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Spdx3ExternalIdentifierType.Cpe23:
|
||||
if (!Cpe23Pattern().IsMatch(extId.Identifier))
|
||||
{
|
||||
warnings.Add(new Spdx3ValidationIssue(
|
||||
"INVALID_CPE23_FORMAT",
|
||||
$"Invalid CPE 2.3 format: {extId.Identifier}",
|
||||
Spdx3ValidationSeverity.Warning,
|
||||
ElementId: element.SpdxId,
|
||||
Path: "externalIdentifier/identifier"));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateHashes(
|
||||
Spdx3Document document,
|
||||
List<Spdx3ValidationIssue> errors,
|
||||
List<Spdx3ValidationIssue> warnings,
|
||||
List<Spdx3ValidationIssue> info)
|
||||
{
|
||||
foreach (var element in document.Elements)
|
||||
{
|
||||
foreach (var integrity in element.VerifiedUsing)
|
||||
{
|
||||
if (integrity is Spdx3Hash hash)
|
||||
{
|
||||
// Validate hex format
|
||||
if (!HexPattern().IsMatch(hash.HashValue))
|
||||
{
|
||||
errors.Add(new Spdx3ValidationIssue(
|
||||
"INVALID_HASH_FORMAT",
|
||||
$"Hash value is not valid hex: {hash.HashValue}",
|
||||
Spdx3ValidationSeverity.Error,
|
||||
ElementId: element.SpdxId,
|
||||
Path: "verifiedUsing/hashValue"));
|
||||
}
|
||||
|
||||
// Validate hash length
|
||||
if (!hash.IsValidLength())
|
||||
{
|
||||
warnings.Add(new Spdx3ValidationIssue(
|
||||
"INVALID_HASH_LENGTH",
|
||||
$"Hash value has unexpected length for {hash.Algorithm}: expected {hash.GetExpectedLength()}, got {hash.HashValue.Length}",
|
||||
Spdx3ValidationSeverity.Warning,
|
||||
ElementId: element.SpdxId,
|
||||
Path: "verifiedUsing/hashValue"));
|
||||
}
|
||||
|
||||
// Warn about deprecated algorithms
|
||||
if (hash.Algorithm.IsDeprecated())
|
||||
{
|
||||
warnings.Add(new Spdx3ValidationIssue(
|
||||
"DEPRECATED_HASH_ALGORITHM",
|
||||
$"Hash algorithm {hash.Algorithm} is deprecated",
|
||||
Spdx3ValidationSeverity.Warning,
|
||||
ElementId: element.SpdxId,
|
||||
Path: "verifiedUsing/algorithm"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user