- Added IIssuerDirectory interface for managing VEX document issuers, including methods for registration, revocation, and trust validation. - Created InMemoryIssuerDirectory class as an in-memory implementation of IIssuerDirectory for testing and single-instance deployments. - Introduced ISignatureVerifier interface for verifying signatures on VEX documents, with support for multiple signature formats. - Developed SignatureVerifier class as the default implementation of ISignatureVerifier, allowing extensibility for different signature formats. - Implemented handlers for DSSE and JWS signature formats, including methods for verification and signature extraction. - Defined various records and enums for issuer and signature metadata, enhancing the structure and clarity of the verification process.
260 lines
8.5 KiB
C#
260 lines
8.5 KiB
C#
using StellaOps.VexLens.Models;
|
|
|
|
namespace StellaOps.VexLens.Mapping;
|
|
|
|
/// <summary>
|
|
/// Utility for matching and comparing product identities across different identifier types.
|
|
/// </summary>
|
|
public static class ProductIdentityMatcher
|
|
{
|
|
/// <summary>
|
|
/// Checks if two products are equivalent based on their identifiers.
|
|
/// </summary>
|
|
public static ProductMatchResult Match(NormalizedProduct product1, NormalizedProduct product2)
|
|
{
|
|
var matches = new List<ProductMatchEvidence>();
|
|
|
|
// Check PURL match
|
|
if (!string.IsNullOrEmpty(product1.Purl) && !string.IsNullOrEmpty(product2.Purl))
|
|
{
|
|
if (PurlParser.IsSamePackage(product1.Purl, product2.Purl))
|
|
{
|
|
var versionMatch = CheckVersionMatch(
|
|
PurlParser.Parse(product1.Purl).PackageUrl?.Version,
|
|
PurlParser.Parse(product2.Purl).PackageUrl?.Version);
|
|
|
|
matches.Add(new ProductMatchEvidence(
|
|
MatchType: ProductMatchType.Purl,
|
|
Confidence: versionMatch ? MatchConfidence.Exact : MatchConfidence.PackageOnly,
|
|
Evidence: $"PURL match: {product1.Purl} ≈ {product2.Purl}"));
|
|
}
|
|
}
|
|
|
|
// Check CPE match
|
|
if (!string.IsNullOrEmpty(product1.Cpe) && !string.IsNullOrEmpty(product2.Cpe))
|
|
{
|
|
if (CpeParser.IsSameProduct(product1.Cpe, product2.Cpe))
|
|
{
|
|
var cpe1 = CpeParser.Parse(product1.Cpe).Cpe;
|
|
var cpe2 = CpeParser.Parse(product2.Cpe).Cpe;
|
|
var versionMatch = cpe1?.Version == cpe2?.Version && cpe1?.Version != "*";
|
|
|
|
matches.Add(new ProductMatchEvidence(
|
|
MatchType: ProductMatchType.Cpe,
|
|
Confidence: versionMatch ? MatchConfidence.Exact : MatchConfidence.PackageOnly,
|
|
Evidence: $"CPE match: {product1.Cpe} ≈ {product2.Cpe}"));
|
|
}
|
|
}
|
|
|
|
// Check key match
|
|
if (!string.IsNullOrEmpty(product1.Key) && !string.IsNullOrEmpty(product2.Key))
|
|
{
|
|
if (string.Equals(product1.Key, product2.Key, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
matches.Add(new ProductMatchEvidence(
|
|
MatchType: ProductMatchType.Key,
|
|
Confidence: MatchConfidence.Exact,
|
|
Evidence: $"Key match: {product1.Key}"));
|
|
}
|
|
}
|
|
|
|
// Check name + version match
|
|
if (!string.IsNullOrEmpty(product1.Name) && !string.IsNullOrEmpty(product2.Name))
|
|
{
|
|
if (string.Equals(product1.Name, product2.Name, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var versionMatch = CheckVersionMatch(product1.Version, product2.Version);
|
|
matches.Add(new ProductMatchEvidence(
|
|
MatchType: ProductMatchType.NameVersion,
|
|
Confidence: versionMatch ? MatchConfidence.Exact : MatchConfidence.PackageOnly,
|
|
Evidence: $"Name match: {product1.Name}" + (versionMatch ? $" @ {product1.Version}" : "")));
|
|
}
|
|
}
|
|
|
|
// Check hash match
|
|
if (product1.Hashes != null && product2.Hashes != null)
|
|
{
|
|
foreach (var (alg, hash1) in product1.Hashes)
|
|
{
|
|
if (product2.Hashes.TryGetValue(alg, out var hash2))
|
|
{
|
|
if (string.Equals(hash1, hash2, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
matches.Add(new ProductMatchEvidence(
|
|
MatchType: ProductMatchType.Hash,
|
|
Confidence: MatchConfidence.Exact,
|
|
Evidence: $"Hash match ({alg}): {hash1}"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine overall match result
|
|
var overallConfidence = matches.Count > 0
|
|
? matches.Max(m => m.Confidence)
|
|
: MatchConfidence.None;
|
|
|
|
return new ProductMatchResult(
|
|
IsMatch: matches.Count > 0,
|
|
OverallConfidence: overallConfidence,
|
|
Evidence: matches);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds matching products in a collection.
|
|
/// </summary>
|
|
public static IReadOnlyList<ProductMatchResult> FindMatches(
|
|
NormalizedProduct target,
|
|
IEnumerable<NormalizedProduct> candidates,
|
|
MatchConfidence minimumConfidence = MatchConfidence.PackageOnly)
|
|
{
|
|
var results = new List<ProductMatchResult>();
|
|
|
|
foreach (var candidate in candidates)
|
|
{
|
|
var matchResult = Match(target, candidate);
|
|
if (matchResult.IsMatch && matchResult.OverallConfidence >= minimumConfidence)
|
|
{
|
|
results.Add(matchResult with { MatchedProduct = candidate });
|
|
}
|
|
}
|
|
|
|
return results.OrderByDescending(r => r.OverallConfidence).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes a similarity score between two products (0.0 to 1.0).
|
|
/// </summary>
|
|
public static double ComputeSimilarity(NormalizedProduct product1, NormalizedProduct product2)
|
|
{
|
|
var matchResult = Match(product1, product2);
|
|
|
|
if (!matchResult.IsMatch)
|
|
{
|
|
return 0.0;
|
|
}
|
|
|
|
return matchResult.OverallConfidence switch
|
|
{
|
|
MatchConfidence.Exact => 1.0,
|
|
MatchConfidence.PackageOnly => 0.8,
|
|
MatchConfidence.Fuzzy => 0.5,
|
|
MatchConfidence.Partial => 0.3,
|
|
_ => 0.0
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects the identifier type from a string.
|
|
/// </summary>
|
|
public static ProductIdentifierType? DetectIdentifierType(string? identifier)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(identifier))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (identifier.StartsWith("pkg:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return PurlParser.IsValid(identifier) ? ProductIdentifierType.Purl : null;
|
|
}
|
|
|
|
if (identifier.StartsWith("cpe:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return CpeParser.IsValid(identifier) ? ProductIdentifierType.Cpe : null;
|
|
}
|
|
|
|
if (identifier.StartsWith("swid:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return ProductIdentifierType.Swid;
|
|
}
|
|
|
|
// Could be a bom-ref or vendor product ID
|
|
return ProductIdentifierType.Custom;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts all identifiers from a product.
|
|
/// </summary>
|
|
public static IReadOnlyList<(ProductIdentifierType Type, string Value)> ExtractIdentifiers(NormalizedProduct product)
|
|
{
|
|
var identifiers = new List<(ProductIdentifierType, string)>();
|
|
|
|
if (!string.IsNullOrWhiteSpace(product.Purl))
|
|
{
|
|
identifiers.Add((ProductIdentifierType.Purl, product.Purl));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(product.Cpe))
|
|
{
|
|
identifiers.Add((ProductIdentifierType.Cpe, product.Cpe));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(product.Key))
|
|
{
|
|
var keyType = DetectIdentifierType(product.Key);
|
|
if (keyType.HasValue && keyType.Value != ProductIdentifierType.Purl && keyType.Value != ProductIdentifierType.Cpe)
|
|
{
|
|
identifiers.Add((keyType.Value, product.Key));
|
|
}
|
|
else if (keyType == null)
|
|
{
|
|
identifiers.Add((ProductIdentifierType.Custom, product.Key));
|
|
}
|
|
}
|
|
|
|
return identifiers;
|
|
}
|
|
|
|
private static bool CheckVersionMatch(string? version1, string? version2)
|
|
{
|
|
if (string.IsNullOrEmpty(version1) || string.IsNullOrEmpty(version2))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return string.Equals(version1, version2, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of a product match operation.
|
|
/// </summary>
|
|
public sealed record ProductMatchResult(
|
|
bool IsMatch,
|
|
MatchConfidence OverallConfidence,
|
|
IReadOnlyList<ProductMatchEvidence> Evidence,
|
|
NormalizedProduct? MatchedProduct = null);
|
|
|
|
/// <summary>
|
|
/// Evidence supporting a product match.
|
|
/// </summary>
|
|
public sealed record ProductMatchEvidence(
|
|
ProductMatchType MatchType,
|
|
MatchConfidence Confidence,
|
|
string Evidence);
|
|
|
|
/// <summary>
|
|
/// Type of product match.
|
|
/// </summary>
|
|
public enum ProductMatchType
|
|
{
|
|
Purl,
|
|
Cpe,
|
|
Key,
|
|
NameVersion,
|
|
Hash
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confidence level of a match.
|
|
/// </summary>
|
|
public enum MatchConfidence
|
|
{
|
|
None = 0,
|
|
Partial = 1,
|
|
Fuzzy = 2,
|
|
PackageOnly = 3,
|
|
Exact = 4
|
|
}
|