898 lines
33 KiB
C#
898 lines
33 KiB
C#
// <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.Build;
|
|
using StellaOps.Spdx3.Model.Security;
|
|
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 readonly TimeProvider _timeProvider;
|
|
|
|
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,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
_contextResolver = contextResolver;
|
|
_logger = logger;
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
}
|
|
|
|
/// <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),
|
|
"Build" or "build_Build" or "spdx:Build" =>
|
|
ParseBuild(element, spdxId),
|
|
"security_Vulnerability" or "Vulnerability" or "spdx:security_Vulnerability" =>
|
|
ParseVulnerability(element, spdxId),
|
|
"security_VexAffectedVulnAssessmentRelationship" or
|
|
"security_VexNotAffectedVulnAssessmentRelationship" or
|
|
"security_VexFixedVulnAssessmentRelationship" or
|
|
"security_VexUnderInvestigationVulnAssessmentRelationship" =>
|
|
ParseVexAssessment(element, spdxId, type),
|
|
"security_CvssV3VulnAssessmentRelationship" =>
|
|
ParseCvssAssessment(element, spdxId),
|
|
"security_EpssVulnAssessmentRelationship" =>
|
|
ParseEpssAssessment(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 Spdx3Build ParseBuild(JsonElement element, string spdxId)
|
|
{
|
|
// Parse timestamps
|
|
DateTimeOffset? buildStartTime = null;
|
|
DateTimeOffset? buildEndTime = null;
|
|
|
|
var startTimeStr = GetStringProperty(element, "build_buildStartTime");
|
|
if (!string.IsNullOrEmpty(startTimeStr) && DateTimeOffset.TryParse(startTimeStr, out var parsedStart))
|
|
{
|
|
buildStartTime = parsedStart;
|
|
}
|
|
|
|
var endTimeStr = GetStringProperty(element, "build_buildEndTime");
|
|
if (!string.IsNullOrEmpty(endTimeStr) && DateTimeOffset.TryParse(endTimeStr, out var parsedEnd))
|
|
{
|
|
buildEndTime = parsedEnd;
|
|
}
|
|
|
|
// Parse config source digests
|
|
var configSourceDigests = ImmutableArray<Spdx3BuildHash>.Empty;
|
|
if (element.TryGetProperty("build_configSourceDigest", out var digestsElement) &&
|
|
digestsElement.ValueKind == JsonValueKind.Array)
|
|
{
|
|
var digests = new List<Spdx3BuildHash>();
|
|
foreach (var digestEl in digestsElement.EnumerateArray())
|
|
{
|
|
if (digestEl.ValueKind == JsonValueKind.Object)
|
|
{
|
|
var algorithm = GetStringProperty(digestEl, "algorithm") ?? "sha256";
|
|
var hashValue = GetStringProperty(digestEl, "hashValue") ?? string.Empty;
|
|
digests.Add(new Spdx3BuildHash { Algorithm = algorithm, HashValue = hashValue });
|
|
}
|
|
}
|
|
configSourceDigests = digests.ToImmutableArray();
|
|
}
|
|
|
|
// Parse environment and parameters as dictionaries
|
|
var environment = ParseDictionary(element, "build_environment");
|
|
var parameters = ParseDictionary(element, "build_parameter");
|
|
|
|
return new Spdx3Build
|
|
{
|
|
SpdxId = spdxId,
|
|
Type = GetStringProperty(element, "@type"),
|
|
Name = GetStringProperty(element, "name"),
|
|
Summary = GetStringProperty(element, "summary"),
|
|
Description = GetStringProperty(element, "description"),
|
|
BuildType = GetStringProperty(element, "build_buildType") ?? string.Empty,
|
|
BuildId = GetStringProperty(element, "build_buildId"),
|
|
BuildStartTime = buildStartTime,
|
|
BuildEndTime = buildEndTime,
|
|
ConfigSourceUri = GetStringArrayProperty(element, "build_configSourceUri"),
|
|
ConfigSourceDigest = configSourceDigests,
|
|
ConfigSourceEntrypoint = GetStringArrayProperty(element, "build_configSourceEntrypoint"),
|
|
Environment = environment,
|
|
Parameter = parameters,
|
|
VerifiedUsing = ParseIntegrityMethods(element),
|
|
ExternalRef = ParseExternalRefs(element),
|
|
ExternalIdentifier = ParseExternalIdentifiers(element)
|
|
};
|
|
}
|
|
|
|
private static ImmutableDictionary<string, string> ParseDictionary(JsonElement element, string propertyName)
|
|
{
|
|
if (!element.TryGetProperty(propertyName, out var dictElement) ||
|
|
dictElement.ValueKind != JsonValueKind.Object)
|
|
{
|
|
return ImmutableDictionary<string, string>.Empty;
|
|
}
|
|
|
|
var dict = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
foreach (var property in dictElement.EnumerateObject())
|
|
{
|
|
if (property.Value.ValueKind == JsonValueKind.String)
|
|
{
|
|
dict[property.Name] = property.Value.GetString() ?? string.Empty;
|
|
}
|
|
}
|
|
|
|
return dict.ToImmutableDictionary();
|
|
}
|
|
|
|
private Spdx3Vulnerability ParseVulnerability(JsonElement element, string spdxId)
|
|
{
|
|
DateTimeOffset? publishedTime = null;
|
|
DateTimeOffset? modifiedTime = null;
|
|
DateTimeOffset? withdrawnTime = null;
|
|
|
|
var publishedStr = GetStringProperty(element, "security_publishedTime");
|
|
if (!string.IsNullOrEmpty(publishedStr) && DateTimeOffset.TryParse(publishedStr, out var parsedPublished))
|
|
{
|
|
publishedTime = parsedPublished;
|
|
}
|
|
|
|
var modifiedStr = GetStringProperty(element, "security_modifiedTime");
|
|
if (!string.IsNullOrEmpty(modifiedStr) && DateTimeOffset.TryParse(modifiedStr, out var parsedModified))
|
|
{
|
|
modifiedTime = parsedModified;
|
|
}
|
|
|
|
var withdrawnStr = GetStringProperty(element, "security_withdrawnTime");
|
|
if (!string.IsNullOrEmpty(withdrawnStr) && DateTimeOffset.TryParse(withdrawnStr, out var parsedWithdrawn))
|
|
{
|
|
withdrawnTime = parsedWithdrawn;
|
|
}
|
|
|
|
return new Spdx3Vulnerability
|
|
{
|
|
SpdxId = spdxId,
|
|
Type = GetStringProperty(element, "@type"),
|
|
Name = GetStringProperty(element, "name"),
|
|
Summary = GetStringProperty(element, "summary"),
|
|
Description = GetStringProperty(element, "description"),
|
|
PublishedTime = publishedTime,
|
|
ModifiedTime = modifiedTime,
|
|
WithdrawnTime = withdrawnTime,
|
|
ExternalRefs = ParseExternalRefs(element),
|
|
ExternalIdentifiers = ParseExternalIdentifiers(element)
|
|
};
|
|
}
|
|
|
|
private Spdx3VulnAssessmentRelationship ParseVexAssessment(
|
|
JsonElement element,
|
|
string spdxId,
|
|
string type)
|
|
{
|
|
var assessedElement = GetStringProperty(element, "security_assessedElement") ?? string.Empty;
|
|
var from = GetStringProperty(element, "from") ?? string.Empty;
|
|
var toValue = GetStringProperty(element, "to");
|
|
var toArray = toValue != null ? ImmutableArray.Create(toValue) : GetStringArrayProperty(element, "to");
|
|
|
|
DateTimeOffset? publishedTime = null;
|
|
var publishedStr = GetStringProperty(element, "security_publishedTime");
|
|
if (!string.IsNullOrEmpty(publishedStr) && DateTimeOffset.TryParse(publishedStr, out var parsedPublished))
|
|
{
|
|
publishedTime = parsedPublished;
|
|
}
|
|
|
|
DateTimeOffset? actionStatementTime = null;
|
|
var actionTimeStr = GetStringProperty(element, "security_actionStatementTime");
|
|
if (!string.IsNullOrEmpty(actionTimeStr) && DateTimeOffset.TryParse(actionTimeStr, out var parsedActionTime))
|
|
{
|
|
actionStatementTime = parsedActionTime;
|
|
}
|
|
|
|
var vexVersion = GetStringProperty(element, "security_vexVersion");
|
|
var statusNotes = GetStringProperty(element, "security_statusNotes");
|
|
var actionStatement = GetStringProperty(element, "security_actionStatement");
|
|
var impactStatement = GetStringProperty(element, "security_impactStatement");
|
|
var suppliedBy = GetStringProperty(element, "security_suppliedBy");
|
|
|
|
// Parse justification for not_affected
|
|
Spdx3VexJustificationType? justificationType = null;
|
|
var justificationStr = GetStringProperty(element, "security_justificationType");
|
|
if (!string.IsNullOrEmpty(justificationStr) &&
|
|
Enum.TryParse<Spdx3VexJustificationType>(justificationStr, ignoreCase: true, out var parsed))
|
|
{
|
|
justificationType = parsed;
|
|
}
|
|
|
|
return type switch
|
|
{
|
|
"security_VexAffectedVulnAssessmentRelationship" => new Spdx3VexAffectedVulnAssessmentRelationship
|
|
{
|
|
SpdxId = spdxId,
|
|
Type = type,
|
|
AssessedElement = assessedElement,
|
|
From = from,
|
|
To = toArray,
|
|
RelationshipType = Spdx3RelationshipType.Affects,
|
|
VexVersion = vexVersion,
|
|
StatusNotes = statusNotes,
|
|
ActionStatement = actionStatement,
|
|
ActionStatementTime = actionStatementTime,
|
|
PublishedTime = publishedTime,
|
|
SuppliedBy = suppliedBy
|
|
},
|
|
"security_VexNotAffectedVulnAssessmentRelationship" => new Spdx3VexNotAffectedVulnAssessmentRelationship
|
|
{
|
|
SpdxId = spdxId,
|
|
Type = type,
|
|
AssessedElement = assessedElement,
|
|
From = from,
|
|
To = toArray,
|
|
RelationshipType = Spdx3RelationshipType.DoesNotAffect,
|
|
VexVersion = vexVersion,
|
|
StatusNotes = statusNotes,
|
|
ImpactStatement = impactStatement,
|
|
JustificationType = justificationType,
|
|
PublishedTime = publishedTime,
|
|
SuppliedBy = suppliedBy
|
|
},
|
|
"security_VexFixedVulnAssessmentRelationship" => new Spdx3VexFixedVulnAssessmentRelationship
|
|
{
|
|
SpdxId = spdxId,
|
|
Type = type,
|
|
AssessedElement = assessedElement,
|
|
From = from,
|
|
To = toArray,
|
|
RelationshipType = Spdx3RelationshipType.FixedIn,
|
|
VexVersion = vexVersion,
|
|
StatusNotes = statusNotes,
|
|
PublishedTime = publishedTime,
|
|
SuppliedBy = suppliedBy
|
|
},
|
|
"security_VexUnderInvestigationVulnAssessmentRelationship" => new Spdx3VexUnderInvestigationVulnAssessmentRelationship
|
|
{
|
|
SpdxId = spdxId,
|
|
Type = type,
|
|
AssessedElement = assessedElement,
|
|
From = from,
|
|
To = toArray,
|
|
RelationshipType = Spdx3RelationshipType.UnderInvestigationFor,
|
|
VexVersion = vexVersion,
|
|
StatusNotes = statusNotes,
|
|
PublishedTime = publishedTime,
|
|
SuppliedBy = suppliedBy
|
|
},
|
|
_ => throw new ArgumentException($"Unknown VEX assessment type: {type}")
|
|
};
|
|
}
|
|
|
|
private Spdx3CvssV3VulnAssessmentRelationship ParseCvssAssessment(JsonElement element, string spdxId)
|
|
{
|
|
var assessedElement = GetStringProperty(element, "security_assessedElement") ?? string.Empty;
|
|
var from = GetStringProperty(element, "from") ?? string.Empty;
|
|
var toValue = GetStringProperty(element, "to");
|
|
var toArray = toValue != null ? ImmutableArray.Create(toValue) : GetStringArrayProperty(element, "to");
|
|
|
|
decimal? baseScore = null;
|
|
if (element.TryGetProperty("security_score", out var scoreEl) && scoreEl.TryGetDecimal(out var score))
|
|
{
|
|
baseScore = score;
|
|
}
|
|
|
|
var vectorString = GetStringProperty(element, "security_vectorString");
|
|
|
|
// Parse severity enum
|
|
Spdx3CvssSeverity? severityEnum = null;
|
|
var severityStr = GetStringProperty(element, "security_severity");
|
|
if (!string.IsNullOrEmpty(severityStr) &&
|
|
Enum.TryParse<Spdx3CvssSeverity>(severityStr, ignoreCase: true, out var parsedSeverity))
|
|
{
|
|
severityEnum = parsedSeverity;
|
|
}
|
|
|
|
return new Spdx3CvssV3VulnAssessmentRelationship
|
|
{
|
|
SpdxId = spdxId,
|
|
Type = "security_CvssV3VulnAssessmentRelationship",
|
|
AssessedElement = assessedElement,
|
|
From = from,
|
|
To = toArray,
|
|
RelationshipType = Spdx3RelationshipType.HasAssessmentFor,
|
|
Score = baseScore,
|
|
VectorString = vectorString,
|
|
Severity = severityEnum
|
|
};
|
|
}
|
|
|
|
private Spdx3EpssVulnAssessmentRelationship ParseEpssAssessment(JsonElement element, string spdxId)
|
|
{
|
|
var assessedElement = GetStringProperty(element, "security_assessedElement") ?? string.Empty;
|
|
var from = GetStringProperty(element, "from") ?? string.Empty;
|
|
var toValue = GetStringProperty(element, "to");
|
|
var toArray = toValue != null ? ImmutableArray.Create(toValue) : GetStringArrayProperty(element, "to");
|
|
|
|
decimal? probability = null;
|
|
if (element.TryGetProperty("security_probability", out var probEl) && probEl.TryGetDecimal(out var prob))
|
|
{
|
|
probability = prob;
|
|
}
|
|
|
|
decimal? percentile = null;
|
|
if (element.TryGetProperty("security_percentile", out var percEl) && percEl.TryGetDecimal(out var perc))
|
|
{
|
|
percentile = perc;
|
|
}
|
|
|
|
return new Spdx3EpssVulnAssessmentRelationship
|
|
{
|
|
SpdxId = spdxId,
|
|
Type = "security_EpssVulnAssessmentRelationship",
|
|
AssessedElement = assessedElement,
|
|
From = from,
|
|
To = toArray,
|
|
RelationshipType = Spdx3RelationshipType.HasAssessmentFor,
|
|
Probability = probability,
|
|
Percentile = percentile
|
|
};
|
|
}
|
|
|
|
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 = _timeProvider.GetUtcNow();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|