218 lines
7.2 KiB
C#
218 lines
7.2 KiB
C#
// <copyright file="CycloneDxEvidenceMapper.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
|
// </copyright>
|
|
|
|
using System.Collections.Immutable;
|
|
using CycloneDX.Models;
|
|
using StellaOps.Scanner.Core.Contracts;
|
|
|
|
namespace StellaOps.Scanner.Emit.Evidence;
|
|
|
|
/// <summary>
|
|
/// Maps StellaOps evidence data to CycloneDX 1.7 native evidence fields.
|
|
/// Sprint: SPRINT_20260107_005_001 Task EV-001
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This mapper replaces the legacy property-based evidence storage with
|
|
/// native CycloneDX 1.7 evidence structures for spec compliance.
|
|
/// </remarks>
|
|
public sealed class CycloneDxEvidenceMapper
|
|
{
|
|
private readonly IdentityEvidenceBuilder _identityBuilder;
|
|
private readonly OccurrenceEvidenceBuilder _occurrenceBuilder;
|
|
private readonly LicenseEvidenceBuilder _licenseBuilder;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CycloneDxEvidenceMapper"/> class.
|
|
/// </summary>
|
|
public CycloneDxEvidenceMapper()
|
|
{
|
|
_identityBuilder = new IdentityEvidenceBuilder();
|
|
_occurrenceBuilder = new OccurrenceEvidenceBuilder();
|
|
_licenseBuilder = new LicenseEvidenceBuilder();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps component evidence to CycloneDX 1.7 Evidence structure.
|
|
/// </summary>
|
|
/// <param name="component">The aggregated component with evidence data.</param>
|
|
/// <returns>The mapped CycloneDX Evidence, or null if no evidence available.</returns>
|
|
public CycloneDX.Models.Evidence? Map(AggregatedComponent component)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(component);
|
|
|
|
var identity = _identityBuilder.Build(component);
|
|
var occurrences = _occurrenceBuilder.Build(component);
|
|
var licenses = _licenseBuilder.Build(component);
|
|
var copyrights = BuildCopyrightEvidence(component);
|
|
|
|
if (identity is null && occurrences.IsDefaultOrEmpty && licenses.IsDefaultOrEmpty && copyrights.IsDefaultOrEmpty)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new CycloneDX.Models.Evidence
|
|
{
|
|
Identity = identity is not null ? [ConvertToEvidenceIdentity(identity)] : null,
|
|
Occurrences = occurrences.IsDefaultOrEmpty ? null : ConvertToEvidenceOccurrences(occurrences),
|
|
Licenses = licenses.IsDefaultOrEmpty ? null : ConvertToLicenseChoices(licenses),
|
|
Copyright = copyrights.IsDefaultOrEmpty ? null : ConvertToEvidenceCopyrights(copyrights),
|
|
};
|
|
}
|
|
|
|
private static EvidenceIdentity ConvertToEvidenceIdentity(ComponentIdentityEvidence identity)
|
|
{
|
|
return new EvidenceIdentity
|
|
{
|
|
// EvidenceIdentity.Field is a string in some CycloneDX versions
|
|
Confidence = (float?)identity.Confidence,
|
|
ConcludedValue = identity.Field,
|
|
Methods = identity.Methods?.Select(m => new EvidenceMethods
|
|
{
|
|
Confidence = (float?)m.Confidence ?? 0f,
|
|
Value = m.Value,
|
|
}).ToList(),
|
|
};
|
|
}
|
|
|
|
private static List<EvidenceOccurrence> ConvertToEvidenceOccurrences(ImmutableArray<OccurrenceEvidence> occurrences)
|
|
{
|
|
return occurrences.Select(o => new EvidenceOccurrence
|
|
{
|
|
Location = o.Location,
|
|
}).ToList();
|
|
}
|
|
|
|
private static List<LicenseChoice> ConvertToLicenseChoices(ImmutableArray<LicenseEvidence> licenses)
|
|
{
|
|
return licenses.Select(l => l.License).ToList();
|
|
}
|
|
|
|
private static List<EvidenceCopyright> ConvertToEvidenceCopyrights(ImmutableArray<CopyrightEvidence> copyrights)
|
|
{
|
|
return copyrights.Select(c => new EvidenceCopyright
|
|
{
|
|
Text = c.Text,
|
|
}).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps legacy property-based evidence to component evidence data.
|
|
/// </summary>
|
|
/// <param name="properties">Legacy properties containing evidence data.</param>
|
|
/// <returns>Parsed evidence records.</returns>
|
|
public static ImmutableArray<ComponentEvidenceRecord> ParseLegacyProperties(
|
|
IReadOnlyList<Property>? properties)
|
|
{
|
|
if (properties is null || properties.Count == 0)
|
|
{
|
|
return ImmutableArray<ComponentEvidenceRecord>.Empty;
|
|
}
|
|
|
|
var results = ImmutableArray.CreateBuilder<ComponentEvidenceRecord>();
|
|
|
|
foreach (var prop in properties)
|
|
{
|
|
if (prop.Name?.StartsWith("stellaops:evidence[", StringComparison.OrdinalIgnoreCase) == true &&
|
|
!string.IsNullOrWhiteSpace(prop.Value))
|
|
{
|
|
var parsed = ParseLegacyEvidenceValue(prop.Value);
|
|
if (parsed is not null)
|
|
{
|
|
results.Add(parsed);
|
|
}
|
|
}
|
|
}
|
|
|
|
return results.ToImmutable();
|
|
}
|
|
|
|
private static ComponentEvidenceRecord? ParseLegacyEvidenceValue(string value)
|
|
{
|
|
// Format: kind:value@source (e.g., "crypto:aes-256@/src/crypto.c")
|
|
var atIndex = value.LastIndexOf('@');
|
|
if (atIndex <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var kindValue = value[..atIndex];
|
|
var source = value[(atIndex + 1)..];
|
|
|
|
var colonIndex = kindValue.IndexOf(':');
|
|
if (colonIndex <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var kind = kindValue[..colonIndex];
|
|
var evidenceValue = kindValue[(colonIndex + 1)..];
|
|
|
|
return new ComponentEvidenceRecord
|
|
{
|
|
Kind = kind,
|
|
Value = evidenceValue,
|
|
Source = source,
|
|
};
|
|
}
|
|
|
|
private static ImmutableArray<CopyrightEvidence> BuildCopyrightEvidence(AggregatedComponent component)
|
|
{
|
|
if (component.Evidence.IsDefaultOrEmpty)
|
|
{
|
|
return ImmutableArray<CopyrightEvidence>.Empty;
|
|
}
|
|
|
|
var copyrightEvidence = component.Evidence
|
|
.Where(e => string.Equals(e.Kind, "copyright", StringComparison.OrdinalIgnoreCase))
|
|
.Select(e => new CopyrightEvidence { Text = e.Value })
|
|
.ToImmutableArray();
|
|
|
|
return copyrightEvidence;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a parsed component evidence record from legacy or native formats.
|
|
/// Sprint: SPRINT_20260107_005_001 Task EV-001
|
|
/// </summary>
|
|
public sealed record ComponentEvidenceRecord
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the kind of evidence (e.g., "crypto", "license", "copyright").
|
|
/// </summary>
|
|
public required string Kind { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the evidence value (e.g., algorithm name, license ID).
|
|
/// </summary>
|
|
public required string Value { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the source location of the evidence.
|
|
/// </summary>
|
|
public required string Source { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the confidence score (0.0-1.0).
|
|
/// </summary>
|
|
public double? Confidence { get; init; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the detection technique used.
|
|
/// </summary>
|
|
public string? Technique { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// StellaOps internal Copyright Evidence model.
|
|
/// Sprint: SPRINT_20260107_005_001 Task EV-001
|
|
/// </summary>
|
|
public sealed class CopyrightEvidence
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the copyright text.
|
|
/// </summary>
|
|
public string? Text { get; set; }
|
|
}
|