736 lines
25 KiB
C#
736 lines
25 KiB
C#
|
|
using StellaOps.Determinism;
|
|
using StellaOps.VexLens.Models;
|
|
using System.Diagnostics;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.VexLens.Normalization;
|
|
|
|
/// <summary>
|
|
/// Normalizer for CSAF VEX format documents.
|
|
/// CSAF VEX documents follow the OASIS CSAF 2.0 specification with profile "VEX".
|
|
/// </summary>
|
|
public sealed class CsafVexNormalizer : IVexNormalizer
|
|
{
|
|
private readonly IGuidProvider _guidProvider;
|
|
|
|
public CsafVexNormalizer(IGuidProvider? guidProvider = null)
|
|
{
|
|
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
|
}
|
|
|
|
public VexSourceFormat SourceFormat => VexSourceFormat.CsafVex;
|
|
|
|
public bool CanNormalize(string content)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(content))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(content);
|
|
var root = doc.RootElement;
|
|
|
|
// CSAF documents have document.category = "csaf_vex"
|
|
if (root.TryGetProperty("document", out var document))
|
|
{
|
|
if (document.TryGetProperty("category", out var category))
|
|
{
|
|
var categoryStr = category.GetString();
|
|
return categoryStr?.Equals("csaf_vex", StringComparison.OrdinalIgnoreCase) == true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public Task<NormalizationResult> NormalizeAsync(
|
|
string content,
|
|
NormalizationContext context,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var warnings = new List<NormalizationWarning>();
|
|
var statementsSkipped = 0;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(content);
|
|
var root = doc.RootElement;
|
|
|
|
// Extract document metadata
|
|
if (!root.TryGetProperty("document", out var documentElement))
|
|
{
|
|
stopwatch.Stop();
|
|
return Task.FromResult(NormalizationResult.Failed(
|
|
[new NormalizationError("ERR_CSAF_001", "Missing 'document' element", "document", null)],
|
|
new NormalizationMetrics(
|
|
Duration: stopwatch.Elapsed,
|
|
SourceBytes: Encoding.UTF8.GetByteCount(content),
|
|
StatementsExtracted: 0,
|
|
StatementsSkipped: 0,
|
|
ProductsMapped: 0),
|
|
warnings));
|
|
}
|
|
|
|
// Extract document ID
|
|
var documentId = ExtractDocumentId(documentElement);
|
|
if (string.IsNullOrWhiteSpace(documentId))
|
|
{
|
|
documentId = $"csaf:{_guidProvider.NewGuid():N}";
|
|
warnings.Add(new NormalizationWarning(
|
|
"WARN_CSAF_001",
|
|
"Document tracking ID not found; generated a random ID",
|
|
"document.tracking.id"));
|
|
}
|
|
|
|
// Extract issuer from publisher
|
|
var issuer = ExtractIssuer(documentElement, warnings);
|
|
|
|
// Extract timestamps
|
|
var (issuedAt, lastUpdatedAt) = ExtractTimestamps(documentElement);
|
|
|
|
// Extract product tree for product resolution
|
|
var productTree = root.TryGetProperty("product_tree", out var pt) ? pt : default;
|
|
|
|
// Extract vulnerabilities and convert to statements
|
|
var statements = ExtractStatements(root, productTree, warnings, ref statementsSkipped);
|
|
|
|
// Calculate source digest
|
|
var sourceDigest = ComputeDigest(content);
|
|
|
|
// Build provenance
|
|
var provenance = new NormalizationProvenance(
|
|
NormalizedAt: context.NormalizedAt,
|
|
Normalizer: context.Normalizer,
|
|
SourceRevision: null,
|
|
TransformationRules: ["csaf-vex-to-normalized-v1"]);
|
|
|
|
var normalizedDoc = new NormalizedVexDocument(
|
|
SchemaVersion: NormalizedVexDocument.CurrentSchemaVersion,
|
|
DocumentId: documentId,
|
|
SourceFormat: VexSourceFormat.CsafVex,
|
|
SourceDigest: sourceDigest,
|
|
SourceUri: context.SourceUri,
|
|
Issuer: issuer,
|
|
IssuedAt: issuedAt,
|
|
LastUpdatedAt: lastUpdatedAt,
|
|
Statements: statements,
|
|
Provenance: provenance);
|
|
|
|
stopwatch.Stop();
|
|
|
|
return Task.FromResult(NormalizationResult.Successful(
|
|
normalizedDoc,
|
|
new NormalizationMetrics(
|
|
Duration: stopwatch.Elapsed,
|
|
SourceBytes: Encoding.UTF8.GetByteCount(content),
|
|
StatementsExtracted: statements.Count,
|
|
StatementsSkipped: statementsSkipped,
|
|
ProductsMapped: statements.Count),
|
|
warnings));
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
stopwatch.Stop();
|
|
return Task.FromResult(NormalizationResult.Failed(
|
|
[new NormalizationError("ERR_CSAF_002", "Invalid JSON", ex.Path, ex)],
|
|
new NormalizationMetrics(
|
|
Duration: stopwatch.Elapsed,
|
|
SourceBytes: Encoding.UTF8.GetByteCount(content),
|
|
StatementsExtracted: 0,
|
|
StatementsSkipped: 0,
|
|
ProductsMapped: 0),
|
|
warnings));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
stopwatch.Stop();
|
|
return Task.FromResult(NormalizationResult.Failed(
|
|
[new NormalizationError("ERR_CSAF_999", "Unexpected error during normalization", null, ex)],
|
|
new NormalizationMetrics(
|
|
Duration: stopwatch.Elapsed,
|
|
SourceBytes: Encoding.UTF8.GetByteCount(content),
|
|
StatementsExtracted: 0,
|
|
StatementsSkipped: 0,
|
|
ProductsMapped: 0),
|
|
warnings));
|
|
}
|
|
}
|
|
|
|
private static string? ExtractDocumentId(JsonElement document)
|
|
{
|
|
if (document.TryGetProperty("tracking", out var tracking) &&
|
|
tracking.TryGetProperty("id", out var id))
|
|
{
|
|
return id.GetString();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static VexIssuer? ExtractIssuer(JsonElement document, List<NormalizationWarning> warnings)
|
|
{
|
|
if (!document.TryGetProperty("publisher", out var publisher))
|
|
{
|
|
warnings.Add(new NormalizationWarning(
|
|
"WARN_CSAF_002",
|
|
"No publisher found in document",
|
|
"document.publisher"));
|
|
return null;
|
|
}
|
|
|
|
var issuerId = publisher.TryGetProperty("namespace", out var nsProp)
|
|
? nsProp.GetString() ?? "unknown"
|
|
: "unknown";
|
|
|
|
var issuerName = publisher.TryGetProperty("name", out var nameProp)
|
|
? nameProp.GetString() ?? issuerId
|
|
: issuerId;
|
|
|
|
var categoryStr = publisher.TryGetProperty("category", out var catProp)
|
|
? catProp.GetString()
|
|
: null;
|
|
|
|
var category = MapPublisherCategory(categoryStr);
|
|
|
|
return new VexIssuer(
|
|
Id: issuerId,
|
|
Name: issuerName,
|
|
Category: category,
|
|
TrustTier: TrustTier.Unknown,
|
|
KeyFingerprints: null);
|
|
}
|
|
|
|
private static IssuerCategory? MapPublisherCategory(string? category)
|
|
{
|
|
return category?.ToLowerInvariant() switch
|
|
{
|
|
"vendor" => IssuerCategory.Vendor,
|
|
"discoverer" or "coordinator" => IssuerCategory.Community,
|
|
"user" => IssuerCategory.Internal,
|
|
"other" => null,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private static (DateTimeOffset? IssuedAt, DateTimeOffset? LastUpdatedAt) ExtractTimestamps(JsonElement document)
|
|
{
|
|
DateTimeOffset? issuedAt = null;
|
|
DateTimeOffset? lastUpdatedAt = null;
|
|
|
|
if (document.TryGetProperty("tracking", out var tracking))
|
|
{
|
|
if (tracking.TryGetProperty("initial_release_date", out var initialRelease) &&
|
|
initialRelease.ValueKind == JsonValueKind.String)
|
|
{
|
|
if (DateTimeOffset.TryParse(initialRelease.GetString(), out var parsed))
|
|
{
|
|
issuedAt = parsed;
|
|
}
|
|
}
|
|
|
|
if (tracking.TryGetProperty("current_release_date", out var currentRelease) &&
|
|
currentRelease.ValueKind == JsonValueKind.String)
|
|
{
|
|
if (DateTimeOffset.TryParse(currentRelease.GetString(), out var parsed))
|
|
{
|
|
lastUpdatedAt = parsed;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (issuedAt, lastUpdatedAt);
|
|
}
|
|
|
|
private static IReadOnlyList<NormalizedStatement> ExtractStatements(
|
|
JsonElement root,
|
|
JsonElement productTree,
|
|
List<NormalizationWarning> warnings,
|
|
ref int skipped)
|
|
{
|
|
if (!root.TryGetProperty("vulnerabilities", out var vulnerabilities) ||
|
|
vulnerabilities.ValueKind != JsonValueKind.Array)
|
|
{
|
|
warnings.Add(new NormalizationWarning(
|
|
"WARN_CSAF_003",
|
|
"No vulnerabilities array found",
|
|
"vulnerabilities"));
|
|
return [];
|
|
}
|
|
|
|
var statements = new List<NormalizedStatement>();
|
|
var statementIndex = 0;
|
|
|
|
foreach (var vuln in vulnerabilities.EnumerateArray())
|
|
{
|
|
var vulnStatements = ExtractVulnerabilityStatements(
|
|
vuln, productTree, statementIndex, warnings, ref skipped);
|
|
statements.AddRange(vulnStatements);
|
|
statementIndex += vulnStatements.Count;
|
|
}
|
|
|
|
return statements;
|
|
}
|
|
|
|
private static List<NormalizedStatement> ExtractVulnerabilityStatements(
|
|
JsonElement vuln,
|
|
JsonElement productTree,
|
|
int startIndex,
|
|
List<NormalizationWarning> warnings,
|
|
ref int skipped)
|
|
{
|
|
var statements = new List<NormalizedStatement>();
|
|
|
|
// Extract vulnerability ID (CVE or other identifier)
|
|
string? vulnerabilityId = null;
|
|
var aliases = new List<string>();
|
|
|
|
if (vuln.TryGetProperty("cve", out var cve))
|
|
{
|
|
vulnerabilityId = cve.GetString();
|
|
}
|
|
|
|
if (vuln.TryGetProperty("ids", out var ids) && ids.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var id in ids.EnumerateArray())
|
|
{
|
|
if (id.TryGetProperty("text", out var text))
|
|
{
|
|
var idStr = text.GetString();
|
|
if (!string.IsNullOrWhiteSpace(idStr))
|
|
{
|
|
if (vulnerabilityId == null)
|
|
{
|
|
vulnerabilityId = idStr;
|
|
}
|
|
else if (idStr != vulnerabilityId)
|
|
{
|
|
aliases.Add(idStr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(vulnerabilityId))
|
|
{
|
|
warnings.Add(new NormalizationWarning(
|
|
"WARN_CSAF_004",
|
|
"Vulnerability missing CVE or ID; skipped",
|
|
"vulnerabilities[].cve"));
|
|
skipped++;
|
|
return statements;
|
|
}
|
|
|
|
// Extract product_status for VEX statements
|
|
if (!vuln.TryGetProperty("product_status", out var productStatus))
|
|
{
|
|
warnings.Add(new NormalizationWarning(
|
|
"WARN_CSAF_005",
|
|
$"Vulnerability {vulnerabilityId} has no product_status",
|
|
"vulnerabilities[].product_status"));
|
|
return statements;
|
|
}
|
|
|
|
// Process each status category
|
|
var localIndex = 0;
|
|
|
|
// Known not affected
|
|
if (productStatus.TryGetProperty("known_not_affected", out var knownNotAffected) &&
|
|
knownNotAffected.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var productRef in knownNotAffected.EnumerateArray())
|
|
{
|
|
var product = ResolveProduct(productRef, productTree);
|
|
if (product != null)
|
|
{
|
|
var justification = ExtractJustification(vuln, productRef.GetString());
|
|
statements.Add(CreateStatement(
|
|
startIndex + localIndex++,
|
|
vulnerabilityId,
|
|
aliases,
|
|
product,
|
|
VexStatus.NotAffected,
|
|
justification,
|
|
vuln));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fixed
|
|
if (productStatus.TryGetProperty("fixed", out var fixedProducts) &&
|
|
fixedProducts.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var productRef in fixedProducts.EnumerateArray())
|
|
{
|
|
var product = ResolveProduct(productRef, productTree);
|
|
if (product != null)
|
|
{
|
|
statements.Add(CreateStatement(
|
|
startIndex + localIndex++,
|
|
vulnerabilityId,
|
|
aliases,
|
|
product,
|
|
VexStatus.Fixed,
|
|
null,
|
|
vuln));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Known affected
|
|
if (productStatus.TryGetProperty("known_affected", out var knownAffected) &&
|
|
knownAffected.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var productRef in knownAffected.EnumerateArray())
|
|
{
|
|
var product = ResolveProduct(productRef, productTree);
|
|
if (product != null)
|
|
{
|
|
statements.Add(CreateStatement(
|
|
startIndex + localIndex++,
|
|
vulnerabilityId,
|
|
aliases,
|
|
product,
|
|
VexStatus.Affected,
|
|
null,
|
|
vuln));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Under investigation
|
|
if (productStatus.TryGetProperty("under_investigation", out var underInvestigation) &&
|
|
underInvestigation.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var productRef in underInvestigation.EnumerateArray())
|
|
{
|
|
var product = ResolveProduct(productRef, productTree);
|
|
if (product != null)
|
|
{
|
|
statements.Add(CreateStatement(
|
|
startIndex + localIndex++,
|
|
vulnerabilityId,
|
|
aliases,
|
|
product,
|
|
VexStatus.UnderInvestigation,
|
|
null,
|
|
vuln));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Explicit unknown status
|
|
if (productStatus.TryGetProperty("known_unknown", out var knownUnknown) &&
|
|
knownUnknown.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var productRef in knownUnknown.EnumerateArray())
|
|
{
|
|
var product = ResolveProduct(productRef, productTree);
|
|
if (product != null)
|
|
{
|
|
statements.Add(CreateStatement(
|
|
startIndex + localIndex++,
|
|
vulnerabilityId,
|
|
aliases,
|
|
product,
|
|
VexStatus.Unknown,
|
|
null,
|
|
vuln));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (productStatus.TryGetProperty("unknown", out var unknown) &&
|
|
unknown.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var productRef in unknown.EnumerateArray())
|
|
{
|
|
var product = ResolveProduct(productRef, productTree);
|
|
if (product != null)
|
|
{
|
|
statements.Add(CreateStatement(
|
|
startIndex + localIndex++,
|
|
vulnerabilityId,
|
|
aliases,
|
|
product,
|
|
VexStatus.Unknown,
|
|
null,
|
|
vuln));
|
|
}
|
|
}
|
|
}
|
|
|
|
return statements;
|
|
}
|
|
|
|
private static NormalizedProduct? ResolveProduct(JsonElement productRef, JsonElement productTree)
|
|
{
|
|
if (productRef.ValueKind != JsonValueKind.String)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var productId = productRef.GetString();
|
|
if (string.IsNullOrWhiteSpace(productId))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Try to find product details in product_tree
|
|
string? name = null;
|
|
string? version = null;
|
|
string? purl = null;
|
|
string? cpe = null;
|
|
|
|
if (productTree.ValueKind == JsonValueKind.Object)
|
|
{
|
|
// Search in full_product_names
|
|
if (productTree.TryGetProperty("full_product_names", out var fullNames) &&
|
|
fullNames.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var fpn in fullNames.EnumerateArray())
|
|
{
|
|
if (fpn.TryGetProperty("product_id", out var pid) &&
|
|
pid.GetString() == productId)
|
|
{
|
|
name = fpn.TryGetProperty("name", out var n) ? n.GetString() : null;
|
|
|
|
if (fpn.TryGetProperty("product_identification_helper", out var pih))
|
|
{
|
|
purl = pih.TryGetProperty("purl", out var p) ? p.GetString() : null;
|
|
cpe = pih.TryGetProperty("cpe", out var c) ? c.GetString() : null;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Search in branches recursively
|
|
if (name == null && productTree.TryGetProperty("branches", out var branches))
|
|
{
|
|
var result = SearchBranches(branches, productId);
|
|
if (result.HasValue)
|
|
{
|
|
name = result.Value.Name;
|
|
version = result.Value.Version;
|
|
purl = result.Value.Purl;
|
|
cpe = result.Value.Cpe;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new NormalizedProduct(
|
|
Key: productId,
|
|
Name: name,
|
|
Version: version,
|
|
Purl: purl,
|
|
Cpe: cpe,
|
|
Hashes: null);
|
|
}
|
|
|
|
private static (string? Name, string? Version, string? Purl, string? Cpe)? SearchBranches(
|
|
JsonElement branches,
|
|
string productId)
|
|
{
|
|
if (branches.ValueKind != JsonValueKind.Array)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (var branch in branches.EnumerateArray())
|
|
{
|
|
// Check product in this branch
|
|
if (branch.TryGetProperty("product", out var product) &&
|
|
product.TryGetProperty("product_id", out var pid) &&
|
|
pid.GetString() == productId)
|
|
{
|
|
var name = product.TryGetProperty("name", out var n) ? n.GetString() : null;
|
|
var version = branch.TryGetProperty("name", out var bn) &&
|
|
branch.TryGetProperty("category", out var bc) &&
|
|
bc.GetString() == "product_version"
|
|
? bn.GetString()
|
|
: null;
|
|
|
|
string? purl = null;
|
|
string? cpe = null;
|
|
|
|
if (product.TryGetProperty("product_identification_helper", out var pih))
|
|
{
|
|
purl = pih.TryGetProperty("purl", out var p) ? p.GetString() : null;
|
|
cpe = pih.TryGetProperty("cpe", out var c) ? c.GetString() : null;
|
|
}
|
|
|
|
return (name, version, purl, cpe);
|
|
}
|
|
|
|
// Recurse into sub-branches
|
|
if (branch.TryGetProperty("branches", out var subBranches))
|
|
{
|
|
var result = SearchBranches(subBranches, productId);
|
|
if (result.HasValue)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static VexJustification? ExtractJustification(JsonElement vuln, string? productId)
|
|
{
|
|
// Look for flags that indicate justification
|
|
if (!vuln.TryGetProperty("flags", out var flags) ||
|
|
flags.ValueKind != JsonValueKind.Array)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
foreach (var flag in flags.EnumerateArray())
|
|
{
|
|
// Check if this flag applies to our product
|
|
if (flag.TryGetProperty("product_ids", out var productIds) &&
|
|
productIds.ValueKind == JsonValueKind.Array)
|
|
{
|
|
var applies = false;
|
|
foreach (var pid in productIds.EnumerateArray())
|
|
{
|
|
if (pid.GetString() == productId)
|
|
{
|
|
applies = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!applies)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (flag.TryGetProperty("label", out var label))
|
|
{
|
|
var labelStr = label.GetString();
|
|
var justification = MapCsafFlagToJustification(labelStr);
|
|
if (justification.HasValue)
|
|
{
|
|
return justification;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static VexJustification? MapCsafFlagToJustification(string? label)
|
|
{
|
|
return label?.ToLowerInvariant() switch
|
|
{
|
|
"component_not_present" => VexJustification.ComponentNotPresent,
|
|
"vulnerable_code_not_present" => VexJustification.VulnerableCodeNotPresent,
|
|
"vulnerable_code_not_in_execute_path" or "vulnerable_code_cannot_be_controlled_by_adversary" =>
|
|
VexJustification.VulnerableCodeNotInExecutePath,
|
|
"inline_mitigations_already_exist" => VexJustification.InlineMitigationsAlreadyExist,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private static NormalizedStatement CreateStatement(
|
|
int index,
|
|
string vulnerabilityId,
|
|
List<string> aliases,
|
|
NormalizedProduct product,
|
|
VexStatus status,
|
|
VexJustification? justification,
|
|
JsonElement vuln)
|
|
{
|
|
// Extract notes for status notes
|
|
string? statusNotes = null;
|
|
if (vuln.TryGetProperty("notes", out var notes) && notes.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var note in notes.EnumerateArray())
|
|
{
|
|
if (note.TryGetProperty("category", out var cat) &&
|
|
cat.GetString() == "description" &&
|
|
note.TryGetProperty("text", out var text))
|
|
{
|
|
statusNotes = text.GetString();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract action statement from remediations
|
|
string? actionStatement = null;
|
|
DateTimeOffset? actionTimestamp = null;
|
|
|
|
if (vuln.TryGetProperty("remediations", out var remediations) &&
|
|
remediations.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var rem in remediations.EnumerateArray())
|
|
{
|
|
if (rem.TryGetProperty("details", out var details))
|
|
{
|
|
actionStatement = details.GetString();
|
|
}
|
|
|
|
if (rem.TryGetProperty("date", out var date) &&
|
|
date.ValueKind == JsonValueKind.String)
|
|
{
|
|
if (DateTimeOffset.TryParse(date.GetString(), out var parsed))
|
|
{
|
|
actionTimestamp = parsed;
|
|
}
|
|
}
|
|
|
|
break; // Take first remediation
|
|
}
|
|
}
|
|
|
|
// Extract release date as timestamp
|
|
DateTimeOffset? timestamp = null;
|
|
if (vuln.TryGetProperty("release_date", out var releaseDate) &&
|
|
releaseDate.ValueKind == JsonValueKind.String)
|
|
{
|
|
if (DateTimeOffset.TryParse(releaseDate.GetString(), out var parsed))
|
|
{
|
|
timestamp = parsed;
|
|
}
|
|
}
|
|
|
|
return new NormalizedStatement(
|
|
StatementId: $"stmt-{index}",
|
|
VulnerabilityId: vulnerabilityId,
|
|
VulnerabilityAliases: aliases.Count > 0 ? aliases : null,
|
|
Product: product,
|
|
Status: status,
|
|
StatusNotes: statusNotes,
|
|
Justification: justification,
|
|
ImpactStatement: null,
|
|
ActionStatement: actionStatement,
|
|
ActionStatementTimestamp: actionTimestamp,
|
|
Versions: null,
|
|
Subcomponents: null,
|
|
FirstSeen: timestamp,
|
|
LastSeen: timestamp);
|
|
}
|
|
|
|
private static string ComputeDigest(string content)
|
|
{
|
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
|
|
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
|
}
|
|
}
|