Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Emit/Evidence/LegacyEvidencePropertyWriter.cs

164 lines
5.3 KiB
C#

// <copyright file="LegacyEvidencePropertyWriter.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
// </copyright>
using System.Collections.Immutable;
using System.Globalization;
using CycloneDX.Models;
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Emit.Evidence;
/// <summary>
/// Writes evidence data to legacy CycloneDX property format for backward compatibility.
/// Sprint: SPRINT_20260107_005_001 Task EV-009
/// </summary>
/// <remarks>
/// <para>Migration Support:</para>
/// <para>
/// During the migration period from property-based evidence to native CycloneDX 1.7 evidence fields,
/// this class provides dual-output capability - writing evidence both to native fields and legacy properties.
/// </para>
/// <para>Legacy Property Format:</para>
/// <list type="bullet">
/// <item>stellaops:evidence[n]:kind - Evidence kind (identity, occurrence, license, callstack)</item>
/// <item>stellaops:evidence[n]:source - Evidence source/analyzer</item>
/// <item>stellaops:evidence[n]:value - Evidence value</item>
/// <item>stellaops:evidence[n]:confidence - Confidence score (0.0-1.0)</item>
/// <item>stellaops:evidence[n]:methods - Reference to evidence.methods[] (CycloneDX 1.7)</item>
/// </list>
/// </remarks>
public sealed class LegacyEvidencePropertyWriter
{
private const string PropertyPrefix = "stellaops:evidence";
/// <summary>
/// Writes component evidence to legacy property format.
/// </summary>
/// <param name="component">The CycloneDX component to add properties to.</param>
/// <param name="evidence">The evidence collection from Scanner core.</param>
/// <param name="options">Options controlling evidence output.</param>
public void WriteEvidenceProperties(
Component component,
ImmutableArray<ComponentEvidence> evidence,
LegacyEvidenceOptions options)
{
ArgumentNullException.ThrowIfNull(component);
if (evidence.IsDefaultOrEmpty)
{
return;
}
component.Properties ??= [];
int evidenceIndex = 0;
foreach (var item in evidence)
{
WriteEvidenceItem(component.Properties, item, evidenceIndex, options);
evidenceIndex++;
}
}
/// <summary>
/// Removes legacy evidence properties from a component.
/// </summary>
/// <param name="component">The CycloneDX component to clean.</param>
public void RemoveLegacyProperties(Component component)
{
ArgumentNullException.ThrowIfNull(component);
if (component.Properties == null)
{
return;
}
component.Properties.RemoveAll(p => p.Name?.StartsWith(PropertyPrefix, StringComparison.Ordinal) == true);
}
private void WriteEvidenceItem(
List<Property> properties,
ComponentEvidence evidence,
int index,
LegacyEvidenceOptions options)
{
var prefix = $"{PropertyPrefix}[{index.ToString(CultureInfo.InvariantCulture)}]";
// Kind
properties.Add(new Property
{
Name = $"{prefix}:kind",
Value = evidence.Kind,
});
// Source
if (!string.IsNullOrWhiteSpace(evidence.Source))
{
properties.Add(new Property
{
Name = $"{prefix}:source",
Value = evidence.Source,
});
}
// Value
if (!string.IsNullOrWhiteSpace(evidence.Value))
{
properties.Add(new Property
{
Name = $"{prefix}:value",
Value = evidence.Value,
});
}
// Methods reference (CycloneDX 1.7 interop)
if (options.IncludeMethodsReference)
{
var methodsReference = MapKindToMethodsReference(evidence.Kind);
if (!string.IsNullOrWhiteSpace(methodsReference))
{
properties.Add(new Property
{
Name = $"{prefix}:methods",
Value = methodsReference,
});
}
}
}
private static string? MapKindToMethodsReference(string kind)
{
return kind.ToLowerInvariant() switch
{
"identity" => "evidence.identity",
"occurrence" => "evidence.occurrences",
"license" => "evidence.licenses",
"callstack" => "evidence.callstack",
"copyright" => "evidence.copyright",
"hash" => "evidence.identity",
"manifest" => "evidence.occurrences",
"signature" => "evidence.identity",
_ => null,
};
}
}
/// <summary>
/// Options for legacy evidence property output.
/// </summary>
public sealed class LegacyEvidenceOptions
{
/// <summary>
/// Gets or sets a value indicating whether to include references to CycloneDX 1.7 evidence.methods[].
/// Default is true.
/// </summary>
public bool IncludeMethodsReference { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether legacy properties should be written at all.
/// When false, only native CycloneDX 1.7 evidence fields are used.
/// Default is true during migration period.
/// </summary>
public bool EnableLegacyProperties { get; set; } = true;
}