more audit work
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
// <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; }
|
||||
}
|
||||
Reference in New Issue
Block a user